From 415dff84a6327bb2a58825f0dacc4c0141c4f766 Mon Sep 17 00:00:00 2001 From: "bruno.silva" Date: Thu, 6 Aug 2026 10:38:35 +0100 Subject: [PATCH 1/3] Release: 2.29.1 --- CHANGELOG.md | 20 ++++++ CLAUDE.md | 95 +++++++++++++++++++++++++++++ android/build.gradle.kts | 2 +- package-lock.json | 4 +- package.json | 4 +- sample/ios/Podfile.lock | 128 +++++++++++++++++++-------------------- 6 files changed, 184 insertions(+), 69 deletions(-) create mode 100644 CLAUDE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2170340..1e2c97e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,24 @@ [Release Notes](https://docs.usercentrics.com/cmp_in_app_sdk/latest/about/history/) +### 2.29.1 – Aug 5, 2026 +## Features +* **[Unity]** Added `isExempt` to the Unity `ServiceConsent` model, propagating the statistical exception flag to the Unity bridge + +### 2.29.0 – Aug 5, 2026 +## Features +* **[Mediation]** Added Amazon Consent Signal (ACS) mediation support for Android and iOS, including Adjust adapter integration +* **[PUR Compliance]** Added statistical exception disclosure and toggle UI to the first and second layer, and excluded statistical exception services from Reject All/Deny All, supporting the UK DUAA statistical-exception requirement +* **[Unity]** Automated the Unity-flavored SPM release pipeline, publishing `usercentrics-spm-ui`/`usercentrics-spm-sdk` package tags for Unity releases + +## Fixes +* **[Android]** Fixed consent banner rendering behind system bars on edge-to-edge displays +* **[Android]** Fixed `ExternalLinkSpan.onClick` crash on invalid/empty link URLs +* **[iOS]** Fixed app hang caused by synchronous HTML parsing in first-layer banner/content text +* **[Android/iOS]** Removed residual `assert(false)` calls in Additional Consent Mode and CCPA save paths that could crash debug builds +* **[TV]** Fixed `DPAD_DOWN` navigation escaping the second-layer detail list boundary + +## Chores +* **[Unity]** Stripped debug symbols from iOS core release binaries, reducing device binary size by ~31% with no functional change + ### 2.28.2 – Jul 22, 2026 ## Fixes * **[iOS]** Rebuilt `UsercentricsUI.xcframework` with the Xcode 26 / Swift 6.3.3 toolchain, fixing build failures on Xcode 26.6 for apps consuming 2.27.1/2.28.0 (including via the React Native bridge) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..280a52d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# CLAUDE.md + +Guidance for Claude Code when working in this repo. This file is read automatically — keep it accurate as the release process evolves. + +## Repo purpose + +`@usercentrics/react-native-sdk` — React Native bridge/wrapper around the native Usercentrics iOS (`UsercentricsUI`/`Usercentrics` CocoaPods) and Android (`com.usercentrics.sdk:usercentrics-ui`) SDKs. The `sample/` directory is a full RN sample app used to build and test the native bridges (Xcode workspace under `sample/ios`, Gradle project under `sample/android`). + +## Release process ("faz a release X.Y.Z") + +When asked to prepare/cut a release, treat it as a pure **version bump** — the underlying native SDK versions (iOS `Usercentrics`/`UsercentricsUI`, Android `usercentrics-ui`) must already be published on CocoaPods trunk / Maven before you start, and the CHANGELOG entry for the new version is normally written by the user beforehand. Verify both before touching anything (see checklist). + +### 1. Pre-flight checklist + +- [ ] Confirm CHANGELOG.md already has a `### X.Y.Z – , ` section at the top (see format below). If missing, ask — don't invent changelog content. +- [ ] Confirm you're on a dedicated branch (e.g. `bump/X.Y.Z`), never commit version bumps directly to `master`. +- [ ] Confirm the target native SDK version is actually resolvable: + - iOS: `bundle exec pod repo update` inside `sample/ios`, then check `~/.cocoapods/repos/cocoapods/Specs/e/f/5/UsercentricsUI/X.Y.Z/` exists (or search CocoaPods trunk). + - Android: the version must exist on Maven Central / the configured repo for `com.usercentrics.sdk:usercentrics-ui`. + - If the native version isn't published yet, stop and say so — bumping to an unpublished version will make `pod install` and Gradle sync fail in CI. + +### 2. Files that encode the version number (bump ALL of them, in this order) + +| File | Field(s) | +|---|---| +| `package.json` | `"version"` **and** `"iosPackageVersion"` (both, they can diverge conceptually but are bumped together) | +| `android/build.gradle.kts` | `val usercentricsVersion = "X.Y.Z"` (line 1) | +| `package-lock.json` | regenerated automatically by `npm install` — don't hand-edit | +| `sample/ios/Podfile.lock` | regenerated by CocoaPods — don't hand-edit (see step 4) | + +CHANGELOG.md format (top of file, newest first): +``` +### X.Y.Z – Mon D, YYYY +## Features +* **[Component]** Description +## Fixes +* **[Component]** Description +## Chores +* **[Component]** Description +``` +`## Features` / `## Fixes` / `## Chores` sections are optional — omit any with nothing to report. + +### 3. Update package/gradle version strings + +Plain string edits — no tooling needed: +```bash +# package.json: bump "version" and "iosPackageVersion" +# android/build.gradle.kts: bump usercentricsVersion +``` + +### 4. Regenerate lockfiles + +```bash +npm install # refreshes package-lock.json "version" fields; also runs `yarn compile` via prepare +cd sample/ios +bundle exec pod repo update # fetch latest trunk specs (required if the native version was just published) +bundle exec pod update UsercentricsUI # bumps UsercentricsUI + Usercentrics + react-native-usercentrics in Podfile.lock +``` +Notes: +- `pod update UsercentricsUI` (not plain `pod install`) is required because the Podfile.lock has the *old* version pinned as a hard constraint — a plain `pod install` will refuse to move it and tell you to run `pod update `. +- The resulting `Podfile.lock` diff will include **a lot of unrelated `SPEC CHECKSUMS` churn** for `React-*` pods even though their versions don't change. This is expected and fine to commit as-is: CI's iOS job always runs `pod deintegrate && pod cache clean --all && bundle exec pod install --repo-update` from scratch (see `.github/workflows/ci.yml`), so whatever you commit here gets regenerated by CI anyway — it is not validated byte-for-byte. Sanity-check it's deterministic by running `pod install` a second time and confirming no further diff. +- The `sample` app's own `package-lock.json`/`node_modules` are independent from the root ones and are normally **not** touched by a version bump (the sample doesn't pin the SDK version itself, it consumes it via a local path dependency). + +### 5. Run tests before pushing (don't skip — this is what keeps CI green) + +```bash +npm run compile # tsc -p . — must be clean +npm test # jest src/__tests__/index.test.ts — must be 100% passing +``` +- `npm run lint` currently has **57 pre-existing errors / ~4700 warnings on master** — this is known tech debt, unrelated to version bumps. Don't try to fix it as part of a release; just confirm the error count didn't *increase* vs. `master` (`git stash` and re-run to compare if in doubt). +- Full native test suites (`xcodebuild test` on `sampleTests`, `./gradlew koverXmlReport`) are what CI's `test-ios` / `test-android` jobs run — they're slow (multi-minute Xcode builds) and CI already re-resolves pods from scratch, so don't try to fully reproduce them locally as a gate. It's fine to let CI be the source of truth for those; just make sure the pod resolution step (`pod install`) itself succeeds locally first, since that's the part a bad version bump most commonly breaks. +- `scripts/assert_export.sh` (run in CI's `test-rn` job) checks every file in `src/models/` is re-exported from `src/models/index.tsx` — irrelevant to a pure version bump but worth knowing it exists if a release also lands new model files. + +### 6. Commit + +Match the existing convention exactly (see `cb74f4b2`, `3380b40`, `7b13655`, `ae95d6e` for precedent): +``` +Release: X.Y.Z + +Co-Authored-By: Claude Sonnet 5 +``` +Files in the commit: `CHANGELOG.md`, `package.json`, `package-lock.json`, `android/build.gradle.kts`, `sample/ios/Podfile.lock`. Nothing else — a release commit should never carry unrelated code changes. + +### 7. After merge + +Actual `npm publish` happens via `.github/workflows/release.yml`, triggered by a **GitHub Release being created** (not by this commit landing on master). That step is out of scope for the bump itself — flag it to the user rather than trying to trigger it. + +## CI reference (`.github/workflows/ci.yml`) + +Runs on every PR into `master`: +- `test-rn` (ubuntu): `yarn install --frozen-lockfile` → `yarn compile` → `yarn test` → `scripts/assert_export.sh` +- `test-ios` (macos-14, Xcode 16.1): fresh `pod deintegrate` + `pod cache clean --all` + `pod install --repo-update`, then `xcodebuild test` on the `sampleTests` scheme +- `test-android` (macOS-14, Java 17): `./gradlew :react-native-usercentrics:koverXmlReport` + +`RCT_NEW_ARCH_ENABLED` in the iOS job's `env:` is actually irrelevant — `sample/ios/Podfile` hardcodes `$RCT_NEW_ARCH_ENABLED = true` and sets `ENV['RCT_NEW_ARCH_ENABLED']` *from* that, not the other way round. Always assume New Architecture (Fabric + TurboModules) is on for both local and CI pod resolution. diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 0158856..6d23d4a 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,4 +1,4 @@ -val usercentricsVersion = "2.28.2" +val usercentricsVersion = "2.29.1" val reactNativeVersion = "+" fun BooleanProperty(name: String): Boolean { diff --git a/package-lock.json b/package-lock.json index 27e4984..5516ec9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@usercentrics/react-native-sdk", - "version": "2.28.2", + "version": "2.29.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@usercentrics/react-native-sdk", - "version": "2.28.2", + "version": "2.29.1", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@babel/core": "^7.25.10", diff --git a/package.json b/package.json index 54c25d4..a34a2b0 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "@usercentrics/react-native-sdk", - "version": "2.28.2", + "version": "2.29.1", "description": "Usercentrics SDK", "homepage": "https://usercentrics.com", "main": "lib/index.js", "types": "lib/index.d.ts", "author": "Usercentrics ", "iosPackageName": "react-native-usercentrics", - "iosPackageVersion": "2.28.2", + "iosPackageVersion": "2.29.1", "license": "SEE LICENSE IN LICENSE", "files": [ "android", diff --git a/sample/ios/Podfile.lock b/sample/ios/Podfile.lock index f7ff60c..3bd39a9 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.28.2): + - react-native-usercentrics (2.29.1): - boost - DoubleConversion - fast_float @@ -1951,7 +1951,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket - - UsercentricsUI (= 2.28.2) + - UsercentricsUI (= 2.29.1) - Yoga - react-native-webview (13.16.1): - boost @@ -2546,9 +2546,9 @@ PODS: - SocketRocket - Yoga - SocketRocket (0.7.1) - - Usercentrics (2.28.2) - - UsercentricsUI (2.28.2): - - Usercentrics (= 2.28.2) + - Usercentrics (2.29.1) + - UsercentricsUI (2.29.1): + - Usercentrics (= 2.29.1) - Yoga (0.0.0) DEPENDENCIES: @@ -2822,70 +2822,70 @@ SPEC CHECKSUMS: RCTTypeSafety: 4aefa8328ab1f86da273f08517f1f6b343f6c2cc React: 2073376f47c71b7e9a0af7535986a77522ce1049 React-callinvoker: 751b6f2c83347a0486391c3f266f291f0f53b27e - React-Core: 7195661f0b48e7ea46c3360ccb575288a20c932c - React-CoreModules: 14f0054ab46000dd3b816d6528af3bd600d82073 - React-cxxreact: 7f602425c63096c398dac13cd7a300efd7c281ae + React-Core: dff5d29973349b11dd6631c9498456d75f846d5e + React-CoreModules: c0ae04452e4c5d30e06f8e94692a49107657f537 + React-cxxreact: 376fd672c95dfb64ad5cc246e6a1e9edb78dec4c React-debug: d4955c86870792887ed695df6ebf0e94e39dc7e1 - React-defaultsnativemodule: e741702f0e585c2f252cf1797ae7556312a5e43f - React-domnativemodule: 71832948d5efe4231231929f3ab8fb43c60e64be - React-Fabric: 40b52987bbf49a5eb3963d69eb79ee5fb474497d - React-FabricComponents: ac181f57440b220bc5c0c73a213c8f0beb4b402c - React-FabricImage: c32725d2935166d14fb6a0248ad5eec890a6665b - React-featureflags: f9cadeda57aa490c9c7a1df9af6866ef68bbddf6 - React-featureflagsnativemodule: 2c4196feb481fe502e4549bf8cff78cb98514b59 - React-graphics: b9a2c17b8baafe92ab5aad8ba940c30428cf9c99 - React-hermes: 0a167bbb02c242664745e82154578c64e90a88e5 - React-idlecallbacksnativemodule: 0950653cf076a6f98fe33403a70f9ab8506940bc - React-ImageManager: f2f1f5496db3912ebbc166701a381cea102123fd - React-jserrorhandler: ca36f91ee924e45aee9c14e5529ef7b94dcbfb8f - React-jsi: 9c27d27d3007b73c702ad3fd5a6166557c741020 - React-jsiexecutor: 2b24f4ed4026344a27f717bf947a434cbbeeff7a - React-jsinspector: 4bba4426916dbad83fd71eef70350cdf6bac70d1 - React-jsinspectorcdp: 2bde8377dc70d07c213c270135aaf3e9b660d6df - React-jsinspectornetwork: 25a94605232a7f5b9e74f54a1422a69baecf0517 - React-jsinspectortracing: ccae54ad4669316451af1297cc6cbd731a098ca5 - React-jsitooling: 754bebd7e20c271797bfa0df835b33dacfbf4821 - React-jsitracing: 339c27481f2fa42c0a71afcec86cf46022fdbf20 - React-logger: 1767babce2d28c3251039ce05556714a2c8c6ded - React-Mapbuffer: f84e59c14ff145295fbd029c5be16805aabe98d2 - React-microtasksnativemodule: 584eb07c9b1f1e684fe63b7fae61ed865f8f228f - react-native-safe-area-context: 7e0ba374906d8f5009aaf96cd19d4866d8de342b - react-native-usercentrics: 581b7026e7cc7d6a14a18ee1ee3f5f1add8f81b2 - react-native-webview: 21fdd62caca650645e429b4a84941626612616ef - React-NativeModulesApple: dcfbe72c5a47baec0699a2935c080b7de0c8657b + React-defaultsnativemodule: bd2b805c6daa85d430d034aa748544b377ada152 + React-domnativemodule: b5c04a4a74ed9c3cb25adc72583b017868600464 + React-Fabric: 93a9ff378f1edf29e9a22a24ad55a1be061e7985 + React-FabricComponents: 83bd54366d4ecb8bec563aa1a78d49915763d503 + React-FabricImage: 8bcd88e553047d4ed5c7ea3def8d6c0e3dd88cfc + React-featureflags: 4ea691ab154d505277859416aa226ae32edeef5f + React-featureflagsnativemodule: b8f00b01436294a30dc62fb5e50b70aa3910309c + React-graphics: d6207795fe822668daeb9c6e1f1470a8500d9eec + React-hermes: fcbdc45ecf38259fe3b12642bd0757c52270a107 + React-idlecallbacksnativemodule: f390a518e1a862453f45f86a1bc248350634d858 + React-ImageManager: acb99e093632b7fc2953dd45f2abaeeea2d9588e + React-jserrorhandler: 958ab9afbe7acdbfe8ca225f7503313409b1319a + React-jsi: 59ec3190dd364cca86a58869e7755477d2468948 + React-jsiexecutor: b87d78a2e8dd7a6f56e9cdac038da45de98c944f + React-jsinspector: 9c33e0c4eeeb10a23b61c4501947b57977980e0e + React-jsinspectorcdp: d7b2c3feddd3669f0eaad2ac1e0f7afbc1d1cf18 + React-jsinspectornetwork: 696d0cf07016e69c053deffba30003fa448904a3 + React-jsinspectortracing: 05d49cd8795db15a279eab6f7604dfa9fe9622f1 + React-jsitooling: 0f9894c3656c3c13d4fcfe6e1dc964fd340acf49 + React-jsitracing: dc11027f9e4e829d32bf17626ec831581ea05223 + React-logger: a3cb5b29c32b8e447b5a96919340e89334062b48 + React-Mapbuffer: e4a65db5f4df53369f39558c0cf2f480f6d3d6c7 + React-microtasksnativemodule: 86334c5c06315e0bccb7b6e6f2c905e92f98b615 + react-native-safe-area-context: eda63a662750758c1fdd7e719c9f1026c8d161cb + react-native-usercentrics: 8cd13b2cfe938140f93bc9296ac764a1d5ae4f02 + react-native-webview: 83c663c5bdf1357d3e7c00986260cb888ea0e328 + React-NativeModulesApple: 8c7eb6057b00c191a11ad5ced41826ec5a0e4d78 React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d - React-perflogger: a03d913e3205b00aee4128082abe42fd45ce0c98 - React-performancetimeline: e07fcee93986259c74a5be1a98770ed82086fe5b + React-perflogger: 5536d2df3d18fe0920263466f7b46a56351c0510 + React-performancetimeline: c6c9393c1a0453a51e1852e3531defe60790b36c React-RCTActionSheet: 42195ae666e6d79b4af2346770f765b7c29435b9 - React-RCTAnimation: 5c10527683128c56ff2c09297fb080f7c35bd293 - React-RCTAppDelegate: c616bd5b0d12f0b21dfacee9cd2d512c6df013aa - React-RCTBlob: 6e3757bdd7dce6fd9788c0dd675fd6b6c432db9d - React-RCTFabric: 25825d88450a5a076f8a31282f3ad745741283f4 - React-RCTFBReactNativeSpec: 672c5e8f3b94bd17979df3ef27ef84bfd4317a5a - React-RCTImage: a3482fe1ae562d1bab08b42d4670a7c9a21813cd - React-RCTLinking: d82b9adb141aef9d2b38d446b837ae7017ab60aa - React-RCTNetwork: fa9350dd99354c5695964f589bd4790bdd4f6a85 - React-RCTRuntime: c52d15ec0e57604245394a1dbef4eb6994716568 - React-RCTSettings: b7f4a03f44dba1d3a4dc6770843547b203ca9129 - React-RCTText: 91dc597a5f6b27fd1048bb287c41ea05eeca9333 - React-RCTVibration: 27b09ddf74bddfa30a58d20e48f885ea6ed6c9d9 + React-RCTAnimation: fa103ccc3503b1ed8dedca7e62e7823937748843 + React-RCTAppDelegate: 665d4baf19424cef08276e9ac0d8771eec4519f9 + React-RCTBlob: 0fa9530c255644db095f2c4fd8d89738d9d9ecc0 + React-RCTFabric: 95eb4a92c5c166e21bae07231d327174e56f202d + React-RCTFBReactNativeSpec: fd66225b71f902a8bfa939fb5f7ec743958298df + React-RCTImage: ba824e61ce2e920a239a65d130b83c3a1d426dff + React-RCTLinking: d2dc199c37e71e6f505d9eca3e5c33be930014d4 + React-RCTNetwork: 87137d4b9bd77e5068f854dd5c1f30d4b072faf6 + React-RCTRuntime: b10bd5e5506af0d6205c4101dd1560fe7beead95 + React-RCTSettings: 71f5c7fd7b5f4e725a4e2114a4b4373d0e46048f + React-RCTText: b94d4699b49285bee22b8ebf768924d607eccee3 + React-RCTVibration: 6e3993c4f6c36a3899059f9a9ead560ddaf5a7d7 React-rendererconsistency: 612d0f6603d9837bb1236d7fd5194203b35c8799 - React-renderercss: 5cc9e5e6732dc124dee16b7ab8f48e0b60b3f31d - React-rendererdebug: 224a1beff9e5d5bc537e72b454135006a5c02a52 - React-RuntimeApple: 9bd8789d7b1d0b5502911da80943b3b2fddfe753 - React-RuntimeCore: 9277538145df1bf2c31432870a308357e34098b2 - React-runtimeexecutor: 69ea4689569738c4ecc4086fde2b30967e19101f - React-RuntimeHermes: 8f59a450f31b741dcf2cc979cb0568a30c5fe1a0 - React-runtimescheduler: 75dfc03be8e0a25751a162acb4ff96be4cc020dc - React-timing: d85ab9efe229cc4145f8f21be0af6c150d3d4682 - React-utils: bb55410c0db3a7f57b9518e3dcf76ab77a0a157e - ReactAppDependencyProvider: b20fba6c3d091a393925890009999472c8f94d95 - ReactCodegen: 07322ec16b66c5f5d7ce7a7cadaba401ecb81908 - ReactCommon: a42100667ef42807c485a579847a5ec2c99e0a82 - RNScreens: 656e050942ae9445f5cc45d05d57f13ce7a4c8e4 + React-renderercss: e5c2c3b84976f7a587cde8423c671db07a6a77da + React-rendererdebug: cc7a6131733605b8897754f72c0c35c79f77da9e + React-RuntimeApple: 3f96102fc1ebf738d36719cdce5422a5769293fb + React-RuntimeCore: f05563107927f155180dfa008fed2ac1316a6aec + React-runtimeexecutor: dd3ec3b76761b43e7b37d07a70de91fc1dd24e7e + React-RuntimeHermes: 7fcb384acc111ea21bcffe2e4a15f31b58bb702e + React-runtimescheduler: 7d2eaa4e7d652a391f47df7ff510260413429bd9 + React-timing: f5d4ba74be96a24b9b2a1a910142ed14e03013d9 + React-utils: eb92d1db56a9bb5911b2c77fb4c2e8d331c8b9dd + ReactAppDependencyProvider: 433ddfb4536948630aadd5bd925aff8a632d2fe3 + ReactCodegen: 2cfa890e84ecf7f3a708f1ed9c0f2c0b22a23c9a + ReactCommon: e9ab32f1d1482d207867b4fdd139361302b9dcc6 + RNScreens: e902eba58a27d3ad399a495d578e8aba3ea0f490 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Usercentrics: d582dd2a4d474f7521fb1c69b0ad2e36306c8f60 - UsercentricsUI: 82c26e8bd40ad89fd4106b53f45ba773eb742429 + Usercentrics: 1234eaa46bfb50c4645226ed0c617ef34967e890 + UsercentricsUI: f304cc98ade660138a90089b7289cb2e09e58d44 Yoga: 9b30b783a17681321b52ac507a37219d7d795ace PODFILE CHECKSUM: 8d257452e9e69d13384a99ee3cd38b42636521da From 59ec45ceb84b5a3aff7e1222f77d208628cf0c1c Mon Sep 17 00:00:00 2001 From: "bruno.silva" Date: Thu, 6 Aug 2026 11:22:01 +0100 Subject: [PATCH 2/3] fix(ios): pass unsavedServiceDecisions param required by Usercentrics 2.29.1 UsercentricsCore.denyAll and denyAllForTCF gained a new unsavedServiceDecisions parameter in the native iOS SDK 2.29.1 (statistical exception support). The Kotlin common interface defaults it to null, but that default isn't carried over to the generated Swift/ObjC interop, so the RN wrapper failed to compile against the bumped native SDK version. Pass nil from RNUsercentricsModule.swift for both call sites, preserving the existing public JS API and behavior (nil means no unsaved service decisions are considered, matching pre-2.29.1 semantics per UsercentricsSDKImpl.denyAll). Updated the FakeUsercentricsManager test mock to match the new protocol signature. Co-Authored-By: Claude Sonnet 5 --- ios/Manager/UsercentricsManager.swift | 12 ++++++------ ios/RNUsercentricsModule.swift | 5 +++-- .../sampleTests/Fake/FakeUsercentricsManager.swift | 8 ++++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/ios/Manager/UsercentricsManager.swift b/ios/Manager/UsercentricsManager.swift index d109560..d62a55a 100644 --- a/ios/Manager/UsercentricsManager.swift +++ b/ios/Manager/UsercentricsManager.swift @@ -33,8 +33,8 @@ public protocol UsercentricsManager { func acceptAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType) -> [UsercentricsServiceConsent] func acceptAll(consentType: UsercentricsConsentType) -> [UsercentricsServiceConsent] - func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?) -> [UsercentricsServiceConsent] - func denyAll(consentType: UsercentricsConsentType) -> [UsercentricsServiceConsent] + func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedServiceDecisions: [String: KotlinBoolean]?) -> [UsercentricsServiceConsent] + func denyAll(consentType: UsercentricsConsentType, unsavedServiceDecisions: [String: KotlinBoolean]?) -> [UsercentricsServiceConsent] func saveDecisionsForTCF(tcfDecisions: TCFUserDecisions, fromLayer: TCFDecisionUILayer, @@ -137,12 +137,12 @@ final class UsercentricsManagerImplementation: UsercentricsManager { return UsercentricsCore.shared.acceptAll(consentType: consentType) } - func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?) -> [UsercentricsServiceConsent] { - return UsercentricsCore.shared.denyAllForTCF(fromLayer: fromLayer, consentType: consentType, unsavedPurposeLIDecisions: unsavedPurposeLIDecisions, unsavedVendorLIDecisions: unsavedVendorLIDecisions) + func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedServiceDecisions: [String: KotlinBoolean]?) -> [UsercentricsServiceConsent] { + return UsercentricsCore.shared.denyAllForTCF(fromLayer: fromLayer, consentType: consentType, unsavedPurposeLIDecisions: unsavedPurposeLIDecisions, unsavedVendorLIDecisions: unsavedVendorLIDecisions, unsavedServiceDecisions: unsavedServiceDecisions) } - func denyAll(consentType: UsercentricsConsentType) -> [UsercentricsServiceConsent] { - return UsercentricsCore.shared.denyAll(consentType: consentType) + func denyAll(consentType: UsercentricsConsentType, unsavedServiceDecisions: [String: KotlinBoolean]?) -> [UsercentricsServiceConsent] { + return UsercentricsCore.shared.denyAll(consentType: consentType, unsavedServiceDecisions: unsavedServiceDecisions) } func saveDecisionsForTCF(tcfDecisions: TCFUserDecisions, fromLayer: TCFDecisionUILayer, serviceDecisions: [UserDecision], consentType: UsercentricsConsentType) -> [UsercentricsServiceConsent] { diff --git a/ios/RNUsercentricsModule.swift b/ios/RNUsercentricsModule.swift index 96ef537..21f7036 100644 --- a/ios/RNUsercentricsModule.swift +++ b/ios/RNUsercentricsModule.swift @@ -202,7 +202,8 @@ class RNUsercentricsModule: RCTEventEmitter { fromLayer: .initialize(from: Int(fromLayer)), consentType: .initialize(from: Int(consentType)), unsavedPurposeLIDecisions: extractLIDecisionsMap(unsavedPurposeLIDecisions), - unsavedVendorLIDecisions: extractLIDecisionsMap(unsavedVendorLIDecisions) + unsavedVendorLIDecisions: extractLIDecisionsMap(unsavedVendorLIDecisions), + unsavedServiceDecisions: nil ) resolve(services.toListOfDictionary()) } @@ -220,7 +221,7 @@ class RNUsercentricsModule: RCTEventEmitter { @objc func denyAll(_ consentType: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { - let services = usercentricsManager.denyAll(consentType: .initialize(from: Int(consentType))) + let services = usercentricsManager.denyAll(consentType: .initialize(from: Int(consentType)), unsavedServiceDecisions: nil) resolve(services.toListOfDictionary()) } diff --git a/sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift b/sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift index 43e59f7..2f1841f 100644 --- a/sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift +++ b/sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift @@ -170,19 +170,23 @@ final class FakeUsercentricsManager: UsercentricsManager { var denyAllForTCFFromLayer: TCFDecisionUILayer? var denyAllForTCFUnsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]? var denyAllForTCFUnsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]? + var denyAllForTCFUnsavedServiceDecisions: [String: KotlinBoolean]? var denyAllForTCFResponse: [UsercentricsServiceConsent]? - func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?) -> [UsercentricsServiceConsent] { + func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedServiceDecisions: [String: KotlinBoolean]?) -> [UsercentricsServiceConsent] { self.denyAllForTCFConsentType = consentType self.denyAllForTCFFromLayer = fromLayer self.denyAllForTCFUnsavedPurposeLIDecisions = unsavedPurposeLIDecisions self.denyAllForTCFUnsavedVendorLIDecisions = unsavedVendorLIDecisions + self.denyAllForTCFUnsavedServiceDecisions = unsavedServiceDecisions return denyAllForTCFResponse! } var denyAllConsentType: UsercentricsConsentType? + var denyAllUnsavedServiceDecisions: [String: KotlinBoolean]? var denyAllResponse: [UsercentricsServiceConsent]? - func denyAll(consentType: UsercentricsConsentType) -> [UsercentricsServiceConsent] { + func denyAll(consentType: UsercentricsConsentType, unsavedServiceDecisions: [String: KotlinBoolean]?) -> [UsercentricsServiceConsent] { self.denyAllConsentType = consentType + self.denyAllUnsavedServiceDecisions = unsavedServiceDecisions return denyAllResponse! } From a1f00c885304e0dabfc8a5285c5dab2db6c28021 Mon Sep 17 00:00:00 2001 From: "bruno.silva" Date: Thu, 6 Aug 2026 11:37:31 +0100 Subject: [PATCH 3/3] fix(ios): add missing isExempt argument to native SDK mock initializers Usercentrics 2.29.1 added a required isExempt parameter to several native init signatures used by test mocks (UsercentricsService, ServiceConsentTemplate, SubConsentTemplate, UsercentricsCategory). Update CMPData+Mock.swift to pass isExempt: false, matching pre-2.29.1 (non-exempt) semantics. Co-Authored-By: Claude Sonnet 5 --- sample/ios/sampleTests/Mock/CMPData+Mock.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sample/ios/sampleTests/Mock/CMPData+Mock.swift b/sample/ios/sampleTests/Mock/CMPData+Mock.swift index 7b138f3..23fc232 100644 --- a/sample/ios/sampleTests/Mock/CMPData+Mock.swift +++ b/sample/ios/sampleTests/Mock/CMPData+Mock.swift @@ -104,7 +104,8 @@ extension UsercentricsService { isAutoUpdateAllowed: true, disableLegalBasis: true, isEssential: true, - metadata: nil) + metadata: nil, + isExempt: false) } } @@ -434,6 +435,7 @@ extension ServiceConsentTemplate { static func mock() -> ServiceConsentTemplate { return .init(isDeactivated: true, defaultConsentStatus: true, + isExempt: false, templateId: "templateId", version: "version", categorySlug: "categorySlug", @@ -452,6 +454,7 @@ extension SubConsentTemplate { static func mock() -> SubConsentTemplate { return .init(isDeactivated: true, defaultConsentStatus: true, + isExempt: false, templateId: "templateId", version: "version", categorySlug: "categorySlug", @@ -468,7 +471,8 @@ extension UsercentricsCategory { label: "label", description: "description", isEssential: true, - isHidden: true) + isHidden: true, + isExempt: false) } }