diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e3b77..d6d9df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,20 @@ [Release Notes](https://docs.usercentrics.com/cmp_in_app_sdk/latest/about/history/) +### 2.32.0 – Sep 30, 2026 +## Features +* **[PUR — Consent or Pay]** Added the Consent-or-Pay 1st-layer banner ("Reject & Subscribe" card, subscriber-login link, `onLoginClicked`/`onSubscribeClicked` callbacks), a setting to control TCF signal retention on rejection plus a subscription-lapse reset API, and a fix so Deny All on the 2nd layer without an active subscription redirects back to the 1st layer instead of silently force-consenting +* **[CTV]** Added an "Accept All" button to the second layer and synced TV SDK behavior with the Admin Interface configuration + +## Fixes +* **[TCF]** Fixed "Show CMP only to EU and EEA users" having no effect when TCF is enabled, which could resurface the banner to non-EU users +* **[Android]** Fixed CMP banner content overlapping camera cutouts and the side navigation bar on edge-to-edge landscape layouts +* **[iOS — Mediation]** Fixed a crash in Consent Mediation with Adjust v5 caused by a selector regression in the Adjust adapter +* **[iOS]** Fixed long TCF first layer non-IAB section titles being truncated instead of wrapping +* **[Unity]** Fixed obsolete `Object.FindObjectOfType(Type)` usage (Unity SDK repo) + +## Chores +* **[Release Engineering]** Unified the native and Unity release into a single CI pipeline run with parallel steps +* **[App Scanner]** Upgraded the Cloud Armor WAF module (CRS 3.3 → 4.22) + ### 2.31.0 – Sep 10, 2026 ## Features * **[Android]** Added edge-to-edge display support for the consent banner, correctly handling system bar insets diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 00b39c7..e9105a2 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,4 +1,4 @@ -val usercentricsVersion = "2.31.0" +val usercentricsVersion = "2.32.0" val reactNativeVersion = "+" fun BooleanProperty(name: String): Boolean { diff --git a/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt b/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt index 08760ff..ef2857a 100644 --- a/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt +++ b/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt @@ -277,6 +277,15 @@ internal class RNUsercentricsModule( }) } + @ReactMethod + override fun notifySubscriptionLapsed(promise: Promise) { + usercentricsProxy.instance.notifySubscriptionLapsed({ + promise.resolve(null) + }, { + promise.reject(it) + }) + } + @ReactMethod override fun addListener(eventName: String) { if (eventName != ON_GPP_SECTION_CHANGE_EVENT) return diff --git a/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt b/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt index 8aa52d7..52e17da 100644 --- a/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt +++ b/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt @@ -39,6 +39,9 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli @ReactMethod abstract fun notifySubscribeSuccess(promise: Promise) + @ReactMethod + abstract fun notifySubscriptionLapsed(promise: Promise) + @ReactMethod abstract fun getConsents(promise: Promise) diff --git a/ios/Manager/UsercentricsManager.swift b/ios/Manager/UsercentricsManager.swift index b108a9c..b18cdca 100644 --- a/ios/Manager/UsercentricsManager.swift +++ b/ios/Manager/UsercentricsManager.swift @@ -15,6 +15,7 @@ public protocol UsercentricsManager { func notifyLoginSuccess(onSuccess: @escaping (() -> Void), onError: @escaping ((Error) -> Void)) func notifySubscribeSuccess(onSuccess: @escaping (() -> Void), onError: @escaping ((Error) -> Void)) + func notifySubscriptionLapsed(onSuccess: @escaping (() -> Void), onError: @escaping ((Error) -> Void)) func showSecondLayer(bannerSettings: BannerSettings?, dismissViewHandler: @escaping (UsercentricsConsentUserResponse) -> Void) @@ -81,6 +82,10 @@ final class UsercentricsManagerImplementation: UsercentricsManager { UsercentricsCore.shared.notifySubscribeSuccess(onSuccess: onSuccess, onError: onError) } + func notifySubscriptionLapsed(onSuccess: @escaping (() -> Void), onError: @escaping ((Error) -> Void)) { + UsercentricsCore.shared.notifySubscriptionLapsed(onSuccess: onSuccess, onError: onError) + } + func showSecondLayer(bannerSettings: BannerSettings?, dismissViewHandler: @escaping (UsercentricsConsentUserResponse) -> Void) { UsercentricsBanner(bannerSettings: bannerSettings).showSecondLayer(completionHandler: dismissViewHandler) diff --git a/ios/RNUsercentricsModule.mm b/ios/RNUsercentricsModule.mm index 7487ec7..f572565 100644 --- a/ios/RNUsercentricsModule.mm +++ b/ios/RNUsercentricsModule.mm @@ -114,4 +114,7 @@ @interface RCT_EXTERN_MODULE(RNUsercentricsModule, NSObject) RCT_EXTERN_METHOD(notifySubscribeSuccess:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(notifySubscriptionLapsed:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) @end diff --git a/ios/RNUsercentricsModule.swift b/ios/RNUsercentricsModule.swift index d4783f3..f341d1d 100644 --- a/ios/RNUsercentricsModule.swift +++ b/ios/RNUsercentricsModule.swift @@ -290,6 +290,14 @@ class RNUsercentricsModule: RCTEventEmitter { } } + @objc func notifySubscriptionLapsed(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + usercentricsManager.notifySubscriptionLapsed { + resolve(nil) + } onError: { error in + reject("usercentrics_reactNative_notifySubscriptionLapsed_error", error.localizedDescription, error) + } + } + private static let onGppSectionChangeEvent = "onGppSectionChange" private static let onLoginClickedEvent = "onLoginClicked" private static let onSubscribeClickedEvent = "onSubscribeClicked" diff --git a/ios/RNUsercentricsModuleSpec.h b/ios/RNUsercentricsModuleSpec.h index 47f99e4..84a22a5 100644 --- a/ios/RNUsercentricsModuleSpec.h +++ b/ios/RNUsercentricsModuleSpec.h @@ -36,6 +36,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)notifySubscribeSuccess:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +- (void)notifySubscriptionLapsed:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject; + // Data Retrieval - (void)getConsents:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; diff --git a/package-lock.json b/package-lock.json index 2de0f13..731230f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@usercentrics/react-native-sdk", - "version": "2.31.0", + "version": "2.32.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@usercentrics/react-native-sdk", - "version": "2.31.0", + "version": "2.32.0", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@babel/core": "^7.25.10", diff --git a/package.json b/package.json index 80c1133..49cdbeb 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "@usercentrics/react-native-sdk", - "version": "2.31.0", + "version": "2.32.0", "description": "Usercentrics SDK", "homepage": "https://usercentrics.com", "main": "lib/index.js", "types": "lib/index.d.ts", "author": "Usercentrics ", "iosPackageName": "react-native-usercentrics", - "iosPackageVersion": "2.31.0", + "iosPackageVersion": "2.32.0", "license": "SEE LICENSE IN LICENSE", "files": [ "android", diff --git a/sample/ios/Podfile b/sample/ios/Podfile index 197c9a1..44edc98 100644 --- a/sample/ios/Podfile +++ b/sample/ios/Podfile @@ -21,6 +21,22 @@ platform :ios, '15.1' use_frameworks! :linkage => :static +# WORKAROUND: Usercentrics.xcframework links `-framework Sentry` eagerly at the binary level +# (a real dynamic dyld load command), but `use_frameworks! :linkage => :static` above makes +# CocoaPods build every pod — including Sentry — as a static archive with no Sentry.framework +# bundle ever embedded in the app. That crashes every launch with "Library not loaded: +# @rpath/Sentry.framework/Sentry". Force only the Sentry pod to build as a dynamic framework +# so the real Sentry.framework exists and gets embedded, matching what the binary expects. +pre_install do |installer| + installer.pod_targets.each do |pod_target| + if pod_target.name.start_with?('Sentry') + def pod_target.build_type + Pod::BuildType.dynamic_framework + end + end + end +end + target 'sample' do config = use_native_modules! @@ -77,7 +93,31 @@ target 'sample' do # C++ settings config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++20' config.build_settings['CLANG_CXX_LIBRARY'] = 'libc++' + + # WORKAROUND (MSDK-4832): native UsercentricsUI.xcframework 2.32.0 (and 2.31.1+) ships + # arm64-only iOS/tvOS Simulator slices — the old x86_64 fat-binary slice was reduced to a + # dSYM-only stub when Sentry was linked in (EXCLUDED_ARCHS added alongside it, unnecessarily: + # the real Sentry-Dynamic xcframework does ship x86_64). Causes a dyld "signal abrt before + # starting test execution" crash on any x86_64 Simulator destination. Exclude x86_64 here + # until a corrected native SDK version is published (tracked in mobile-sdk MSDK-4832). + config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'x86_64' + end + end + + # WORKAROUND (MSDK-4832, continued): excluding x86_64 on the Pods project's own targets + # (above) isn't enough on its own — the "sample" app/test target lives in sample.xcodeproj, + # not Pods.xcodeproj, and Xcode still builds the full arm64+x86_64 ARCHS_STANDARD set for + # "generic/platform=iOS Simulator" destinations regardless of ONLY_ACTIVE_ARCH. Without this, + # xctest still tries to launch an x86_64 slice that links against Pods frameworks with no + # x86_64 slice, crashing with "Test crashed with signal abrt before starting test execution." + # Mirror the exclusion onto the main user project until a corrected native SDK version ships. + installer.aggregate_targets.each do |aggregate_target| + aggregate_target.user_project.native_targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'x86_64' + end end + aggregate_target.user_project.save end # RN 0.81 + Fabric: ReactCodegen can end up with several PBXFileReferences whose path is only diff --git a/sample/ios/Podfile.lock b/sample/ios/Podfile.lock index 3359a18..0c501d5 100644 --- a/sample/ios/Podfile.lock +++ b/sample/ios/Podfile.lock @@ -1924,7 +1924,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-usercentrics (2.31.0): + - react-native-usercentrics (2.32.0): - boost - DoubleConversion - fast_float @@ -1951,7 +1951,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket - - UsercentricsUI (= 2.31.0) + - UsercentricsUI (= 2.32.0) - Yoga - react-native-webview (13.16.1): - boost @@ -2545,10 +2545,14 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga + - Sentry (8.58.4): + - Sentry/Core (= 8.58.4) + - Sentry/Core (8.58.4) - SocketRocket (0.7.1) - - Usercentrics (2.31.0) - - UsercentricsUI (2.31.0): - - Usercentrics (= 2.31.0) + - Usercentrics (2.32.0): + - Sentry (~> 8.52) + - UsercentricsUI (2.32.0): + - Usercentrics (= 2.32.0) - Yoga (0.0.0) DEPENDENCIES: @@ -2642,6 +2646,7 @@ SPEC REPOS: - GoogleUtilities - nanopb - PromisesObjC + - Sentry - SocketRocket - Usercentrics - UsercentricsUI @@ -2850,7 +2855,7 @@ SPEC CHECKSUMS: React-Mapbuffer: e4a65db5f4df53369f39558c0cf2f480f6d3d6c7 React-microtasksnativemodule: 86334c5c06315e0bccb7b6e6f2c905e92f98b615 react-native-safe-area-context: eda63a662750758c1fdd7e719c9f1026c8d161cb - react-native-usercentrics: b5d8232a076e87895a90e1f768d31d4726df5bae + react-native-usercentrics: ac0c8523f802eec858de7ef1dc5fa7b79e92c5c2 react-native-webview: 83c663c5bdf1357d3e7c00986260cb888ea0e328 React-NativeModulesApple: 8c7eb6057b00c191a11ad5ced41826ec5a0e4d78 React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d @@ -2883,11 +2888,12 @@ SPEC CHECKSUMS: ReactCodegen: 2cfa890e84ecf7f3a708f1ed9c0f2c0b22a23c9a ReactCommon: e9ab32f1d1482d207867b4fdd139361302b9dcc6 RNScreens: e902eba58a27d3ad399a495d578e8aba3ea0f490 + Sentry: 5321d74bdd28d6f6f8beaa1ca06f1fdc746bff49 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Usercentrics: 907aa15a6dee4335364105859a1ce0cc4834979d - UsercentricsUI: d7242e2bfc4205135a258d3eb79dc4bf551669ba + Usercentrics: 7bc3a4042e492b9204ed137ce5c6806365e9e0e4 + UsercentricsUI: 0bf3d1a5ff33d34ff98be8645cc0358bc213b37b Yoga: 9b30b783a17681321b52ac507a37219d7d795ace -PODFILE CHECKSUM: 8d257452e9e69d13384a99ee3cd38b42636521da +PODFILE CHECKSUM: 58bddb926e31074bb71fa08e588e08b2cfe944ee COCOAPODS: 1.16.2 diff --git a/sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift b/sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift index 6db7021..0f5d251 100644 --- a/sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift +++ b/sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift @@ -272,4 +272,13 @@ final class FakeUsercentricsManager: UsercentricsManager { } onSuccess() } + + var notifySubscriptionLapsedError: Error? + func notifySubscriptionLapsed(onSuccess: @escaping (() -> Void), onError: @escaping ((Error) -> Void)) { + if let notifySubscriptionLapsedError = notifySubscriptionLapsedError { + onError(notifySubscriptionLapsedError) + return + } + onSuccess() + } } diff --git a/sample/src/screens/Home.tsx b/sample/src/screens/Home.tsx index 01ce7b9..7dd07cf 100644 --- a/sample/src/screens/Home.tsx +++ b/sample/src/screens/Home.tsx @@ -73,6 +73,17 @@ export const HomeScreen = ({ navigation }: { navigation: any }) => { }; }, []); + async function notifySubscriptionLapsed() { + try { + await Usercentrics.notifySubscriptionLapsed(); + console.log('[Usercentrics] notifySubscriptionLapsed done'); + Alert.alert('notifySubscriptionLapsed', 'Subscription reset'); + } catch (e) { + console.error('[Usercentrics] notifySubscriptionLapsed failed:', e); + Alert.alert('notifySubscriptionLapsed failed', String(e)); + } + } + async function showSecondLayer() { try { const response = await Usercentrics.showSecondLayer({ @@ -129,6 +140,7 @@ export const HomeScreen = ({ navigation }: { navigation: any }) => {