diff --git a/purchasely/CHANGELOG.md b/purchasely/CHANGELOG.md index ad196936..ed5a4bc1 100644 --- a/purchasely/CHANGELOG.md +++ b/purchasely/CHANGELOG.md @@ -1,3 +1,16 @@ +## Unreleased + +### Added + +- Added `PLYDataProcessingPurpose.refundHandling` for iOS (requires Purchasely + iOS 6.2.0). It carries the revoked refund-handling consent flag to the + backend. + +### Fixed + +- On iOS, combining `allNonEssentials` with other data processing purposes no + longer collapses the additional purposes. + ## 6.1.0 Brings the Flutter plugin to Purchasely 6.1.0: the anonymous user id, the API diff --git a/purchasely/ios/purchasely_flutter/Classes/SwiftPurchaselyFlutterPlugin.swift b/purchasely/ios/purchasely_flutter/Classes/SwiftPurchaselyFlutterPlugin.swift index ce2b92b1..6f5fa0fe 100644 --- a/purchasely/ios/purchasely_flutter/Classes/SwiftPurchaselyFlutterPlugin.swift +++ b/purchasely/ios/purchasely_flutter/Classes/SwiftPurchaselyFlutterPlugin.swift @@ -1557,20 +1557,18 @@ public class SwiftPurchaselyFlutterPlugin: NSObject, FlutterPlugin { guard let arguments, let purposesArg = arguments["purposes"] as? [String] else { return } - let purposes: Set = if purposesArg.contains("ALL_NON_ESSENTIALS") { - Set([PLYDataProcessingPurpose.allNonEssentials]) - } else { - Set(purposesArg.compactMap { (value: String) -> PLYDataProcessingPurpose? in - switch value { - case "ANALYTICS": .analytics - case "IDENTIFIED_ANALYTICS": .identifiedAnalytics - case "CAMPAIGNS": .campaigns - case "PERSONALIZATION": .personalization - case "THIRD_PARTY_INTEGRATIONS": .thirdPartyIntegrations - default: nil - } - }) - } + let purposes: Set = Set(purposesArg.compactMap { (value: String) -> PLYDataProcessingPurpose? in + switch value { + case "ALL_NON_ESSENTIALS": .allNonEssentials + case "ANALYTICS": .analytics + case "IDENTIFIED_ANALYTICS": .identifiedAnalytics + case "CAMPAIGNS": .campaigns + case "PERSONALIZATION": .personalization + case "THIRD_PARTY_INTEGRATIONS": .thirdPartyIntegrations + case "REFUND_HANDLING": .refundHandling + default: nil + } + }) Purchasely.revokeDataProcessingConsent(for: purposes) } diff --git a/purchasely/lib/purchasely_flutter.dart b/purchasely/lib/purchasely_flutter.dart index aa864f26..450d8fed 100644 --- a/purchasely/lib/purchasely_flutter.dart +++ b/purchasely/lib/purchasely_flutter.dart @@ -740,6 +740,12 @@ class Purchasely { _channel.invokeMethod('clearDynamicOfferings'); } + /// Replaces the persisted set of revoked purposes; it does not merge. + /// + /// Pass the complete list every time. Passing `[]` grants everything back. + /// `allNonEssentials` is analytics, campaigns, personalization, and + /// thirdPartyIntegrations; it excludes identifiedAnalytics and refundHandling, + /// which must be combined explicitly when needed. static void revokeDataProcessingConsent( List purposes) { List mappedPurposes = purposes @@ -949,6 +955,8 @@ class Purchasely { return "PERSONALIZATION"; case PLYDataProcessingPurpose.thirdPartyIntegrations: return "THIRD_PARTY_INTEGRATIONS"; + case PLYDataProcessingPurpose.refundHandling: + return "REFUND_HANDLING"; } } } @@ -1013,7 +1021,13 @@ enum PLYDataProcessingPurpose { identifiedAnalytics, campaigns, personalization, - thirdPartyIntegrations + thirdPartyIntegrations, + + /// iOS only (Purchasely iOS >= 6.2.0); ignored on Android. Carries the + /// revoked-consent flag as `refund-handling` in the + /// `X-PRIVACY-CONSENT-REVOKED-FEATURES` header for refusal of consumption + /// data processing attached to a refund request. Gates no SDK behaviour. + refundHandling } enum PLYThemeMode { light, dark, system } diff --git a/purchasely/test/platform_channel_test.dart b/purchasely/test/platform_channel_test.dart index d6d09602..9c4d8dee 100644 --- a/purchasely/test/platform_channel_test.dart +++ b/purchasely/test/platform_channel_test.dart @@ -698,6 +698,7 @@ void main() { expect(PLYDataProcessingPurpose.campaigns.index, 3); expect(PLYDataProcessingPurpose.personalization.index, 4); expect(PLYDataProcessingPurpose.thirdPartyIntegrations.index, 5); + expect(PLYDataProcessingPurpose.refundHandling.index, 6); }); }); diff --git a/purchasely/test/purchasely_flutter_test.dart b/purchasely/test/purchasely_flutter_test.dart index 9d64e850..2958d719 100644 --- a/purchasely/test/purchasely_flutter_test.dart +++ b/purchasely/test/purchasely_flutter_test.dart @@ -529,6 +529,29 @@ void main() { expect( methodCalls.first.arguments['purposes'], ['ANALYTICS', 'CAMPAIGNS']); }); + + test('revokeDataProcessingConsent maps refundHandling', () { + Purchasely.revokeDataProcessingConsent( + [PLYDataProcessingPurpose.refundHandling]); + + expect(methodCalls.first.arguments['purposes'], ['REFUND_HANDLING']); + }); + + test('revokeDataProcessingConsent preserves combined purposes', () { + Purchasely.revokeDataProcessingConsent([ + PLYDataProcessingPurpose.allNonEssentials, + PLYDataProcessingPurpose.refundHandling + ]); + + expect(methodCalls.first.arguments['purposes'], + ['ALL_NON_ESSENTIALS', 'REFUND_HANDLING']); + }); + + test('revokeDataProcessingConsent clears all purposes', () { + Purchasely.revokeDataProcessingConsent([]); + + expect(methodCalls.first.arguments['purposes'], []); + }); }); group('Transformation Methods', () { @@ -877,6 +900,10 @@ void main() { Purchasely.mapDataProcessingPurposeToString( PLYDataProcessingPurpose.thirdPartyIntegrations), 'THIRD_PARTY_INTEGRATIONS'); + expect( + Purchasely.mapDataProcessingPurposeToString( + PLYDataProcessingPurpose.refundHandling), + 'REFUND_HANDLING'); }); }); @@ -1239,6 +1266,7 @@ void main() { expect(PLYDataProcessingPurpose.campaigns.index, 3); expect(PLYDataProcessingPurpose.personalization.index, 4); expect(PLYDataProcessingPurpose.thirdPartyIntegrations.index, 5); + expect(PLYDataProcessingPurpose.refundHandling.index, 6); }); test('PLYUserAttributeSource has correct values', () {