Skip to content

Commit d46bc2b

Browse files
authored
SDK updates deprecate V1 purchase validation, improve iOS error handling (#429)
* SDK updates deprecate V1 purchase validation, improve iOS error handling SDK Version Updates: - Update Android SDK from 6.17.4 to 6.17.5 - Update iOS SDK from 6.17.7 to 6.17.8 - Update iOS Purchase Connector from 6.17.7 to 6.17.8 - Bump plugin version to 6.17.8 across all platforms API Changes: - Deprecate validateAndLogInAppAndroidPurchase (V1) - Deprecate validateAndLogInAppIosPurchase (V1) - Enhance iOS error handling for validateAndLogInAppPurchaseV2 with NSError parsing (code, domain, userInfo) Documentation Updates: - Remove "Beta" label from validateAndLogInAppPurchaseV2 API - Mark V1 purchase validation APIs as Deprecated - Add comprehensive PlatformException error handling examples - Add iOS token format explanation for uninstall measurement - Add cross-platform Firebase Messaging example for uninstall tokens * lint * CHANGELOG update * fix implementation for validateAndLogInAppPurchaseV2
1 parent 1730b4e commit d46bc2b

13 files changed

Lines changed: 141 additions & 99 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Versions
22

3+
## 6.17.8
4+
5+
- Updated Android SDK from 6.17.4 to 6.17.5
6+
- Updated iOS SDK from 6.17.7 to 6.17.8
7+
- Updated iOS Purchase Connector from 6.17.7 to 6.17.8
8+
- Deprecated `validateAndLogInAppAndroidPurchase` (V1) - use `validateAndLogInAppPurchaseV2` instead
9+
- Deprecated `validateAndLogInAppIosPurchase` (V1) - use `validateAndLogInAppPurchaseV2` instead
10+
- Enhanced iOS error handling for `validateAndLogInAppPurchaseV2` with comprehensive NSError parsing (code, domain, userInfo)
11+
- **Documentation Updates:**
12+
- Removed "Beta" label from `validateAndLogInAppPurchaseV2` API
13+
- Marked V1 purchase validation APIs as Deprecated
14+
- Added comprehensive `PlatformException` error handling examples for V2 API
15+
- Added iOS token format explanation for uninstall measurement
16+
- Added cross-platform Firebase Messaging example for uninstall tokens
17+
318
## 6.17.7+1
419

520
- Update Android SDK version to 6.17.4

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ To do so, please follow [this article](https://support.appsflyer.com/hc/en-us/ar
1111

1212
## SDK Versions
1313

14-
- Android AppsFlyer SDK **v6.17.3**
15-
- iOS AppsFlyer SDK **v6.17.7**
14+
- Android AppsFlyer SDK **v6.17.5**
15+
- iOS AppsFlyer SDK **v6.17.8**
1616

1717
### Purchase Connector versions
1818

1919
- Android 2.2.0
20-
- iOS 6.17.7
20+
- iOS 6.17.8
2121

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

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ android {
5353
dependencies {
5454
implementation fileTree(dir: 'libs', include: ['*.jar'])
5555
implementation 'androidx.appcompat:appcompat:1.0.0'
56-
implementation 'com.appsflyer:af-android-sdk:6.17.4'
56+
implementation 'com.appsflyer:af-android-sdk:6.17.5'
5757
implementation 'com.android.installreferrer:installreferrer:2.2'
5858
// implementation 'androidx.core:core-ktx:1.13.1'
5959
if (includeConnector) {

android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.appsflyer.appsflyersdk;
22

33
public final class AppsFlyerConstants {
4-
final static String PLUGIN_VERSION = "6.17.7+1";
4+
final static String PLUGIN_VERSION = "6.17.8";
55
final static String AF_APP_INVITE_ONE_LINK = "appInviteOneLink";
66
final static String AF_HOST_PREFIX = "hostPrefix";
77
final static String AF_HOST_NAME = "hostName";

doc/AdvancedAPI.md

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,47 @@ You can register the uninstall token with AppsFlyer by calling the following API
3939
appsFlyerSdk.updateServerUninstallToken("token");
4040
```
4141

42+
> **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.
43+
>
44+
> 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.
45+
4246
### Android
4347

4448
It is possible to utilize the [Firebase Messaging Plugin for Flutter](https://pub.dev/packages/firebase_messaging) for everything related to the uninstall token.
4549
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).
4650

47-
On the flutter side, you can register the uninstall token with AppsFlyer by calling the following API with your uninstall token:
51+
On the Flutter side, you can register the uninstall token with AppsFlyer by calling the following API with your uninstall token:
4852
```dart
4953
appsFlyerSdk.updateServerUninstallToken("token");
5054
```
5155

56+
**Example using Firebase Messaging (cross-platform):**
57+
```dart
58+
import 'dart:io' show Platform;
59+
import 'package:firebase_messaging/firebase_messaging.dart';
60+
61+
// Update uninstall token for AppsFlyer
62+
void _updateUninstallToken(appsFlyerSdk) {
63+
if (Platform.isAndroid) {
64+
FirebaseMessaging.instance.getToken().then((token) {
65+
if (token != null) {
66+
appsFlyerSdk.updateServerUninstallToken(token);
67+
}
68+
});
69+
} else if (Platform.isIOS) {
70+
FirebaseMessaging.instance.getAPNSToken().then((token) {
71+
if (token != null) {
72+
appsFlyerSdk.updateServerUninstallToken(token);
73+
}
74+
});
75+
}
76+
}
77+
```
78+
**Note:**
79+
- On Android, `getToken()` returns the FCM token.
80+
- On iOS, `getAPNSToken()` returns the APNs token as a hex string, suitable for `updateServerUninstallToken`.
81+
- Replace `appsFlyerSdk` with your instance of `AppsflyerSdk`.
82+
5283
---
5384

5485
## <a id="user-invite"> User invite
@@ -114,11 +145,9 @@ appsFlyerSdk.generateInviteLink(inviteLinkParams,
114145
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>
115146
Learn more - https://support.appsflyer.com/hc/en-us/articles/207032106-Receipt-validation-for-in-app-purchases<br>
116147

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

119-
> ⚠️ **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.
120-
121-
The new unified purchase validation API that works across both Android and iOS platforms:
150+
The unified purchase validation API that works across both Android and iOS platforms:
122151

123152
```dart
124153
Future<Map<String, dynamic>> validateAndLogInAppPurchaseV2(
@@ -135,7 +164,7 @@ AFPurchaseDetails(
135164
)
136165
```
137166

138-
Example:
167+
**Example:**
139168
```dart
140169
// Create purchase details
141170
AFPurchaseDetails purchaseDetails = AFPurchaseDetails(
@@ -151,24 +180,40 @@ try {
151180
additionalParameters: {"custom_param": "value"}
152181
);
153182
print("Validation successful: $result");
183+
} on PlatformException catch (e) {
184+
// Handle platform-specific errors with detailed information
185+
print("Validation failed: ${e.message}");
186+
print("Error code: ${e.code}");
187+
if (e.details != null) {
188+
// Access detailed error information
189+
final details = e.details as Map<String, dynamic>;
190+
print("Error details: $details");
191+
// On iOS, additional fields may include:
192+
// - error_code: The NSError code
193+
// - error_domain: The NSError domain
194+
// - error_user_info: Additional error context
195+
}
154196
} catch (e) {
155-
print("Validation failed: $e");
197+
print("Unexpected error: $e");
156198
}
157199
```
158200

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

166208
---
167209

168-
**Legacy Platform-Specific APIs:**
210+
**Deprecated Platform-Specific APIs:**
211+
212+
> ⚠️ **Deprecated**: The following platform-specific APIs are deprecated and will be removed in a future version. Please migrate to `validateAndLogInAppPurchaseV2` for cross-platform support.
169213
170-
**Android:**
214+
**Android (Deprecated):**
171215
```dart
216+
@Deprecated('Use validateAndLogInAppPurchaseV2 instead')
172217
Future<dynamic> validateAndLogInAppAndroidPurchase(
173218
String publicKey,
174219
String signature,
@@ -179,6 +224,7 @@ Future<dynamic> validateAndLogInAppAndroidPurchase(
179224
```
180225
Example:
181226
```dart
227+
// Deprecated - migrate to validateAndLogInAppPurchaseV2
182228
appsFlyerSdk.validateAndLogInAppAndroidPurchase(
183229
"publicKey",
184230
"signature",
@@ -188,12 +234,13 @@ appsFlyerSdk.validateAndLogInAppAndroidPurchase(
188234
{"fs": "fs"});
189235
```
190236

191-
**iOS:**
237+
**iOS (Deprecated):**
192238

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

196242
```dart
243+
@Deprecated('Use validateAndLogInAppPurchaseV2 instead')
197244
Future<dynamic> validateAndLogInAppIosPurchase(
198245
String productIdentifier,
199246
String price,
@@ -204,6 +251,7 @@ Future<dynamic> validateAndLogInAppIosPurchase(
204251

205252
Example:
206253
```dart
254+
// Deprecated - migrate to validateAndLogInAppPurchaseV2
207255
appsFlyerSdk.validateAndLogInAppIosPurchase(
208256
"productIdentifier",
209257
"price",

example/ios/Flutter/AppFrameworkInfo.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
<key>CFBundleVersion</key>
2222
<string>1.0</string>
2323
<key>MinimumOSVersion</key>
24-
<string>12.0</string>
24+
<string>13.0</string>
2525
</dict>
2626
</plist>

example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
33
LastUpgradeVersion = "1510"
4-
version = "1.3">
4+
version = "1.7">
55
<BuildAction
66
parallelizeBuildables = "YES"
77
buildImplicitDependencies = "YES">
@@ -44,6 +44,7 @@
4444
buildConfiguration = "Debug"
4545
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
4646
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
47+
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
4748
shouldUseLaunchSchemeArgsEnv = "YES">
4849
<MacroExpansion>
4950
<BuildableReference
@@ -72,6 +73,7 @@
7273
buildConfiguration = "Debug"
7374
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
7475
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
76+
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
7577
launchStyle = "0"
7678
useCustomWorkingDirectory = "NO"
7779
ignoresPersistentStateOnLaunch = "NO"

ios/Classes/AppsflyerSdkPlugin.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
@end
1919

2020
// Appsflyer JS objects
21-
#define kAppsFlyerPluginVersion @"6.17.6"
21+
#define kAppsFlyerPluginVersion @"6.17.8"
2222
#define afDevKey @"afDevKey"
2323
#define afAppId @"afAppId"
2424
#define afIsDebug @"isDebug"

ios/Classes/AppsflyerSdkPlugin.m

Lines changed: 43 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -638,87 +638,56 @@ - (void)validateAndLogInAppPurchase:(FlutterMethodCall*)call result:(FlutterResu
638638
result(nil);
639639
}
640640

641-
- (void)validateAndLogInAppPurchaseV2:(FlutterMethodCall*)call result:(FlutterResult)result{
642-
@try {
643-
// Extract purchase details map from Flutter
644-
NSDictionary* purchaseDetailsMap = call.arguments[@"purchaseDetails"];
645-
NSDictionary* additionalParameters = call.arguments[@"additionalParameters"];
646-
647-
if (purchaseDetailsMap == nil) {
648-
result([FlutterError errorWithCode:@"INVALID_ARGUMENTS"
649-
message:@"Purchase details cannot be null"
650-
details:nil]);
651-
return;
652-
}
653-
654-
// Extract individual fields from purchase details map
655-
NSString* purchaseTypeString = purchaseDetailsMap[@"purchaseType"];
656-
NSString* purchaseToken = purchaseDetailsMap[@"purchaseToken"];
657-
NSString* productId = purchaseDetailsMap[@"productId"];
658-
659-
// Validate required fields
660-
if (purchaseTypeString == nil || purchaseToken == nil || productId == nil) {
661-
result([FlutterError errorWithCode:@"INVALID_ARGUMENTS"
662-
message:@"Purchase details must contain purchaseType, purchaseToken, and productId"
663-
details:nil]);
664-
return;
665-
}
666-
667-
// Map Dart enum values to iOS purchase type
668-
// For iOS, we use transactionId instead of purchaseToken, so we'll use purchaseToken as transactionId
669-
NSString* transactionId = purchaseToken;
670-
671-
NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 called with purchaseType: %@, transactionId: %@, productId: %@", purchaseTypeString, transactionId, productId);
672-
673-
// Call the actual AppsFlyer iOS V2 API
674-
[self callAppsFlyerV2API:purchaseTypeString
675-
transactionId:transactionId
676-
productId:productId
677-
additionalParameters:additionalParameters
678-
result:result];
679-
680-
} @catch (NSException *exception) {
681-
NSLog(@"AppsFlyer: Error in validateAndLogInAppPurchaseV2: %@", exception.reason);
682-
result([FlutterError errorWithCode:@"VALIDATION_ERROR"
683-
message:[NSString stringWithFormat:@"Purchase validation failed: %@", exception.reason]
641+
- (void)validateAndLogInAppPurchaseV2:(FlutterMethodCall*)call result:(FlutterResult)result {
642+
NSDictionary* purchaseDetailsMap = call.arguments[@"purchaseDetails"];
643+
NSDictionary* additionalParameters = call.arguments[@"additionalParameters"];
644+
645+
if (purchaseDetailsMap == nil) {
646+
result([FlutterError errorWithCode:@"INVALID_ARGUMENTS"
647+
message:@"Purchase details cannot be null"
684648
details:nil]);
649+
return;
685650
}
686-
}
687-
688-
- (void)callAppsFlyerV2API:(NSString*)purchaseTypeString
689-
transactionId:(NSString*)transactionId
690-
productId:(NSString*)productId
691-
additionalParameters:(NSDictionary*)additionalParameters
692-
result:(FlutterResult)result {
693651

694-
[[AppsFlyerLib shared] validateAndLogInAppPurchase:productId
695-
price:nil // V2 doesn't use price
696-
currency:nil // V2 doesn't use currency
697-
transactionId:transactionId
698-
additionalParameters:additionalParameters
699-
success:^(NSDictionary *response) {
700-
NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 Success!");
701-
// V2 API returns response directly without wrapper
702-
NSMutableDictionary *v2Response = [NSMutableDictionary dictionaryWithDictionary:response];
703-
v2Response[@"purchase_type"] = purchaseTypeString;
704-
result(v2Response);
652+
NSString* purchaseTypeString = purchaseDetailsMap[@"purchaseType"];
653+
NSString* transactionId = purchaseDetailsMap[@"purchaseToken"]; // purchaseToken maps to transactionId on iOS
654+
NSString* productId = purchaseDetailsMap[@"productId"];
655+
656+
if (purchaseTypeString == nil || transactionId == nil || productId == nil) {
657+
result([FlutterError errorWithCode:@"INVALID_ARGUMENTS"
658+
message:@"Purchase details must contain purchaseType, purchaseToken, and productId"
659+
details:nil]);
660+
return;
705661
}
706-
failure:^(NSError *error, id errorResponse) {
707-
NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 failed with Error: %@", error);
708-
709-
// Create error response for V2 format
710-
NSMutableDictionary *errorData = [NSMutableDictionary dictionary];
662+
663+
// Map Dart enum to iOS AFSDKPurchaseType
664+
AFSDKPurchaseType purchaseType = [purchaseTypeString isEqualToString:@"subscription"]
665+
? AFSDKPurchaseTypeSubscription
666+
: AFSDKPurchaseTypeOneTimePurchase;
667+
668+
AFSDKPurchaseDetails *purchaseDetails = [[AFSDKPurchaseDetails alloc] initWithProductId:productId
669+
transactionId:transactionId
670+
purchaseType:purchaseType];
671+
672+
// Handle NSNull for additionalParameters
673+
NSDictionary* purchaseAdditionalDetails = [additionalParameters isEqual:[NSNull null]] ? nil : additionalParameters;
674+
675+
[[AppsFlyerLib shared] validateAndLogInAppPurchase:purchaseDetails
676+
purchaseAdditionalDetails:purchaseAdditionalDetails
677+
completion:^(NSDictionary * _Nullable response, NSError * _Nullable error) {
711678
if (error) {
712-
errorData[@"error_message"] = error.localizedDescription ?: @"Purchase validation failed";
713-
errorData[@"error_code"] = @(error.code);
714-
}
715-
if (errorResponse && [errorResponse isKindOfClass:[NSDictionary class]]) {
716-
[errorData addEntriesFromDictionary:(NSDictionary*)errorResponse];
679+
NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 failed: %@", error.localizedDescription);
680+
result([FlutterError errorWithCode:@"VALIDATION_ERROR"
681+
message:error.localizedDescription ?: @"Purchase validation failed"
682+
details:@{
683+
@"error_code": @(error.code),
684+
@"error_domain": error.domain ?: @"Unknown"
685+
}]);
686+
return;
717687
}
718688

719-
result([FlutterError errorWithCode:@"VALIDATION_ERROR"
720-
message:error.localizedDescription ?: @"Purchase validation failed"
721-
details:errorData]);
689+
NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 Success!");
690+
result(response);
722691
}];
723692
}
724693

ios/appsflyer_sdk.podspec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'appsflyer_sdk'
3-
s.version = '6.17.7'
3+
s.version = '6.17.8'
44
s.summary = 'AppsFlyer Integration for Flutter'
55
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.'
66
s.homepage = 'https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk'
@@ -21,12 +21,12 @@ Pod::Spec.new do |s|
2121
ss.source_files = 'Classes/**/*'
2222
ss.public_header_files = 'Classes/**/*.h'
2323
ss.dependency 'Flutter'
24-
ss.ios.dependency 'AppsFlyerFramework','6.17.7'
24+
ss.ios.dependency 'AppsFlyerFramework','6.17.8'
2525
end
2626

2727
s.subspec 'PurchaseConnector' do |ss|
2828
ss.dependency 'Flutter'
29-
ss.ios.dependency 'PurchaseConnector', '6.17.7'
29+
ss.ios.dependency 'PurchaseConnector', '6.17.8'
3030
ss.source_files = 'PurchaseConnector/**/*'
3131
ss.public_header_files = 'PurchaseConnector/**/*.h'
3232

0 commit comments

Comments
 (0)