Adapty SDK 4.0.0-beta.2: migrate the JSON layer to Newtonsoft - #29
Open
yauch-dev wants to merge 108 commits into
Open
Adapty SDK 4.0.0-beta.2: migrate the JSON layer to Newtonsoft#29yauch-dev wants to merge 108 commits into
yauch-dev wants to merge 108 commits into
Conversation
…tIfPresent Unwrapping the nullable double without a check threw InvalidOperationException whenever the key was absent, which broke onboarding date_picker events: the contract marks day/month/year optional, so a partially filled date crashed the event handler.
ToJSONObject and ToJSONArray had no branch for either type, so they were
dropped silently: UpdateAttribution(new Dictionary { { "flag", true } }) sent
an empty object. Dates reuse the existing DateTime converter of the SDK.
…ntity IosAppAccountToken is a non-nullable Guid, so comparing it to null was always false and IsEmpty never returned true, leaving the guard in AdaptyConfiguration dead.
The builder field was never copied into AdaptyConfiguration, so server_cluster never reached the native SDK and picking EU or CN was silently ignored. Copying it alone would have crashed activation instead: AdaptyServerCluster.Default serialized to null and JSONObject.Add(key, null) throws. Default now serializes to "default", which the cross-platform contract lists explicitly, and the builder field became nullable so an unset cluster stays absent from the request.
The repository had no tests at all, which makes rewriting ~2.6k lines of hand written serialization risky. These link the SDK sources into a plain .NET project and run with dotnet test, so neither the Unity Editor nor a licence is needed. ModelSnapshot renders a parsed model by reflection - every field including the private ones the SDK serializes, plus computed properties - so the fixtures cover all 155 model fields without listing them, and a field that stops being parsed shows up as a diff. Snapshots are stored per platform because the JSON layer branches on UNITY_IOS and UNITY_ANDROID; run with -p:AdaptyPlatform=UNITY_IOS to pin those. Set ADAPTY_UPDATE_SNAPSHOTS=1 to rewrite approved files.
Matrix over the three platform configurations; received snapshots are uploaded as artifacts when a run fails.
The JSON layer is rewritten on Newtonsoft in a package next to the current one, which is frozen meanwhile. This is the starting copy: C# only, since duplicating the native plugins would mean duplicate symbols on iOS and duplicate classes on Android - the next package drives the native side of the current one. The assembly definition is not auto-referenced, so the demo keeps compiling against the current package until the switch.
…kage AdaptyJson holds the settings the models rely on: NullValueHandling.Ignore to match the manual layer, which could not emit a null at all; DateParseHandling.None so date-looking strings inside payload_data survive as strings. AdaptyContractResolver raises DataMember.IsRequired from Required.AllowNull to Required.Always - Newtonsoft's default would let an explicit null through where the old layer threw. The converters keep the previous observable behaviour: dates read back as local time and written as UTC with milliseconds, enums mapped through EnumMember with a fallback to Unknown where the enum declares one, loose objects as double and nested dictionaries rather than JObject. link.xml is required, not defensive: on an IL2CPP player with stripping High every model fails without it, because the stripper removes constructors of types only ever created by reflection.
The infrastructure tests compare each behaviour against the current layer rather than asserting it in isolation: same instant and same DateTimeKind for dates, same types out of loose dictionaries, zero still written, unknown enum values falling back only where an Unknown member exists. tests/aot-probe holds the procedure and the results of running the same checks on a real IL2CPP player, which is what settles keeping the models' readonly fields.
Default aliases ReloadRevalidatingCacheData but was declared above it, and a static field initializer runs in declaration order - so the field was null and any app passing AdaptyPlacementFetchPolicy.Default hit a NullReferenceException when the request was built. Found by a request snapshot in the migration test suite.
Every model in com.adapty.unity-sdk.next now carries System.Runtime.Serialization
attributes instead of a hand-written +JSON.cs pair. The models stay
serializer-neutral: no Newtonsoft attribute or import reaches Models/, so a later
move to another serializer is a matter of replacing converters.
Four things the attributes cannot express are converters instead: the polymorphic
reads (installation status, onboarding state updates, analytics events), the
nested offer_identifier that the model keeps flat, the custom-asset map that
travels as an array, and the asymmetric paywall product, which goes back to the
native side as a subset of what arrived and gets a request DTO.
AdaptyContractResolver raises IsRequired to Required.Always and honours
ShouldSerializeX for non-public methods and for fields - Newtonsoft looks it up
public-only and only for properties, so on models made of private readonly fields
the convention would silently do nothing and an empty customer identity would
serialize as {}.
Reading and writing are deliberately asymmetric for enums. Six read-path enums
gained a trailing Unknown so a newer native value degrades instead of failing the
response; appending rather than inserting keeps every existing numeric value, and
no enum member in the package is both non-nullable and optional, so Unknown can
never become the value of a missing field. A member without [EnumMember] has no
name in the contract, so it is reachable by a read and rejected by a write.
Verified by 59 golden and 89 parity tests on editor/iOS/Android. The parity suite
holds the new package to the snapshots the current one produces; the enforcement
suite drops one required key at a time, deriving the list from the contract
resolver and crossing the converter boundaries by hand.
Also fixes, in this package: AdaptyServerCluster.Default serialized as "Default",
base_plan_id was wrongly gated to Android, and the null Default fetch policy.
Runtime/JSON/ is deleted. The package no longer contains SimpleJSON and builds without it on editor, iOS and Android. Request.Send takes the request object and the response type, dropping the response-mapper delegate all 40 call sites used to pass. AdaptyResponse parses the success/error envelope and cannot throw out of it: a malformed reply, or one carrying neither member, comes back as DecodingFailed rather than as a default value that would read as "not premium" or "the purchase did not happen". OnMessage is now a thin wrapper around Dispatch with everything inside caught and logged. This is the point of the stage: the call arrives on a reverse-P/Invoke callback with no handler, so an exception there was fatal on IL2CPP instead of surfacing as an error. Payload parsing, a payload of the wrong shape and a throwing listener are all contained. CreateFlowView no longer assembles its optional parameters by hand - the stage-2 annotations already describe them, so the serialized object is merged into the request. UpdateAttribution(Dictionary<string, object>) is one Serialize call, replacing the hand-written converter that silently dropped bool and DateTime before the stage-0a fix. Products go to the native side through AdaptyPaywallProductRequest, not as the 17-field response model. 131 tests, up from 89. The transport suite reads the payload from the far side of the bridge through a seam on the editor stub and pins what 24 public methods send; the event suite covers a real dispatch and six ways a payload can be broken. Both are editor-only - off the editor the bridge is a real P/Invoke with nothing behind it on a desktop host.
The main risk of rewriting a package beside the one it replaces is that a field or an overload quietly disappears: the snapshots stay green, because they only cover what the tests happen to call. Both packages are compiled into their own assembly and read for metadata only - they declare the same type names in the same namespace, so two live definitions of AdaptySDK.Adapty could never be named from one file. The descriptor records what breaks compatibility rather than just names: accessibility including protected internal, static, readonly/const with the constant's value, abstract/sealed, base type and interfaces, accessor presence and accessibility, virtual/override, generic constraints, and parameter direction and defaults. The result is 12 lines, all deliberate: three serialization helpers that had leaked into the public API (one of them an empty class nothing could call), and the six trailing Unknown members from stage 2. Their recorded values confirm each was appended after the existing members and nothing was renumbered. All three platform snapshots are identical, which answers a question the plan left open: no #if in the models changes the public surface. A checker like this fails silently when it stops looking at something, so two fixture assemblies differ only by mutations that keep every member name and type - one category per type or member - and pin that each still produces a diff line. Also fixes .gitignore: the *.csproj rule meant for Unity's generated project files swallowed the hand-written test suites, so tests/ had been committed without a single project file and the CI workflow had nothing to restore.
Running the demo on an IL2CPP player with managed stripping High exposed two defects that every desktop test missed, because nothing is stripped there. [Preserve] on a type keeps the type, its fields and its constructors, but not its methods. Every computed [DataMember] property getter and every ShouldSerialize* was stripped, so activate went out without cross_platform_sdk_name and the native side rejected it: the SDK could not be activated at all. The same hole emptied the purchase request, the flow view's product purchase parameters, profile parameters, customer identity and the custom assets. Fixed by preserving those members individually. Collections declared as IList<T>/IDictionary<K,V> implement neither non-generic IList nor IDictionary, so Newtonsoft populates them through a CollectionWrapper whose constructor it resolves reflectively - and a stripped player no longer has it. That took out AdaptyProfile, and with it activation, access levels and every purchase result. Fixed in the contract resolver, which now contracts the concrete collection, leaving the public interface surface untouched. Both are guarded from here on: StrippingGuardTests checks types and the members reached through a method, AotContractTests asserts no contract asks for a wrapper. Both run on desktop, so CI catches a regression without a device. Verified on an iOS simulator and an Android device, both IL2CPP at stripping High: activation, profile, events, flow, products, flow view with custom assets, and a product request accepted by the store. Restore, onboarding and the permission round-trip are deferred to release acceptance by the owner. StrippingBuild builds the strict configuration reproducibly; it also pins ARM64 and the Android application identifier, without which the Android run cannot be reproduced.
The rewritten package takes over from the one it was built beside: models, transport, events and the Newtonsoft serialization layer move into com.adapty.unity-sdk, and the bundled SimpleJSON goes with the old Runtime/JSON. The package now depends on com.unity.nuget.newtonsoft-json 3.2.2, and an Editor validator reports a missing or duplicated Newtonsoft before it turns into a compile error that does not name the cause. Calling code is unaffected - models, members and method signatures are identical, and the public surface snapshot pins that. The exception is AdaptySDK.SimpleJSON itself, whose types were public and are now gone. Also fixes a defect the device runs uncovered, which predates this work: AdaptyAndroidWrapper posted callbacks to Looper.getMainLooper(), the Android UI thread, while Unity's player loop runs on its own. Every SDK callback therefore arrived on the wrong thread, and any Unity API that checks thread affinity threw - StartCoroutine, SetActive, Instantiate, Destroy. It reached back to 3.3.0. The handler now binds to the registering thread's Looper, which is Unity's, and fails loudly rather than falling back to the wrong one. The wrapper artifact is bumped to 4.0.1, since 4.0.0 is already published. Packaging and CI follow the merge: the .unitypackage exports the root Editor directory as well, so the validator reaches its users, and the test matrix is a single project reading one surface assembly.
CLAUDE.md still described the layer the migration removed: two files per model, ToJSONNode() and GetAdaptyFoo() extension methods, a Runtime/JSON directory with SimpleJSON.cs, and the claim that the project has no CLI test command. The model convention is now stated as it is - [DataContract] and [DataMember] with the JSON key, [Preserve] against managed stripping, and the two enum contracts, since adding [EnumMember] to a numeric enum such as AdaptyErrorCode would silently switch it to strings. The test matrix is documented with the command that runs it. The same staleness sat in the Doxyfile, which excluded AdaptySDK.SimpleJSON from the generated documentation and so no longer excluded anything, letting the internal AdaptySDK.Serialization in; in the iOS cross-reference skill; and in a model header naming its own deleted +JSON.cs file. Test comments and one dead branch still spoke of a package built beside another, which the merge ended. AdaptyStubs.cs went with the golden project it belonged to - it is referenced by no project and no source. No behaviour change: 137 / 95 / 95 pass and the public surface snapshot is unmoved, which is what says the removed branch was dead.
Importing the .unitypackage into a clean project produced 240 CS0246. A .unitypackage carries assets only, so it cannot touch Packages/manifest.json, and Newtonsoft.Json - which the JSON layer now needs - is in no stock Unity template. Every file in the SDK failed to compile, and none of the errors named the cause. The runtime assembly is now gated on the package: versionDefines sets ADAPTY_NEWTONSOFT from com.unity.nuget.newtonsoft-json, and defineConstraints requires it. Without the package the assembly is skipped rather than failing to compile, which is what turns 240 errors into none. The Editor assembly carries no constraint of its own, since it is what has to report the problem and install the fix; its empty references is what keeps it from reaching runtime types it could not compile against. Adapty SDK > Install Dependencies installs whatever is missing - Newtonsoft, and External Dependency Manager along with the OpenUPM scoped registry it is published on. The menu item is permanent rather than conditional: EDM has always had to be installed by hand, Package Manager or not. Scoped registries have no public API, so the registry is written into the manifest as text; AdaptyManifest is that surgery, kept free of Unity so it can be tested, and it declines to edit a manifest it cannot parse rather than guessing. Presence is judged against the package everywhere, not the assembly. A Newtonsoft.Json.dll in Assets/ does not set the version define, so the SDK would silently not compile: the installer refuses to stack a package on top of such a copy, and the validator now names that state instead of reporting success. EDM carries no constraint, so for it any copy counts. ManifestTests covers the text surgery against a strict JSON parser, including the empty root, an empty scopedRegistries and an empty scopes array - the three shapes where a blindly appended comma yields a manifest Unity cannot read.
The package never stated a floor, in package.json or anywhere else, so Package Manager let any Editor install it. 2022.3 is what the Editor-facing code actually assumes: AdaptyDependencies uses Client.AddAndRemove, which arrived after 2020.3, and PackageInfo.FindForAssembly. Nothing is dropped - the floor is now declared instead of being discovered as a compile error in an Editor that was never going to build the package.
4.0 renames the paywall API to flows, adds a package dependency that no Unity template carries, and removes members that were [Obsolete] through 3.x. None of that was written down in one place, and the official documentation page covers only part of it. MIGRATION.md walks the whole move from 3.17: install order and why Newtonsoft has to be there before the SDK compiles, the paywall to flow renames with their signatures, the listener interfaces that gained an I prefix and the ones whose methods were renamed, the removed obsolete members and what replaces them, and what changed for iOS builds - the pod is gone, the workspace is still what you open. Written against the code rather than from memory: view methods live on AdaptyUI and not Adapty, GetFlow gained a locale parameter its predecessor did not have, and AdaptyProductReference has no drop-in replacement, since the type that took its place has a private constructor and internal members.
CHANGELOG.md and the _upm.changelog string say the same thing again, since the latter is what Package Manager shows after an update. The claim that Package Manager installs Newtonsoft for you was true for UPM and false for a .unitypackage, which is how the import blocker went unnoticed; both now say which install path gets what, and name the menu item that covers the other. The note about the SDK reporting the reason is narrowed to when its Editor assembly loads, because it does not while the project's own scripts fail to compile. CLAUDE.md gains the three rules the gate rests on - the constraint on Runtime, the Editor assembly that must compile in every state, and presence judged against the package rather than the assembly - and the version-bump checklist gains the constants in AdaptyDependencies.cs, which drift from package.json otherwise.
The member was commented out in ec38fd7 while the native SDKs kept sending 1004. A device run confirms it still arrives today: RestorePurchases on a profile with nothing to restore returns "Code: 1004, Message: No purchases to restore". AdaptyErrorCode is a numeric contract with no [EnumMember], so the value was never lost - it reached callers as a number that no constant named, leaving them to compare against a literal. Nothing about the error changes, so code already matching 1004 keeps working. The public surface snapshots gain the one member on all three platforms and nothing else, which is what says this is a naming change and not a behaviour one. The three siblings dropped by the same commit - ReceiveRestoredTransactions Failed 1011, PersistingDataError 3100, WrongCallParameter 10001 - stay out. There is no evidence they are still emitted, and restoring them blind would put dead constants in a public API that cannot then be trimmed without a breaking change.
AdaptyErrorCode was compared against both native enums line by line, iOS 4.0.2 and Android 4.0.1, and it had fallen behind both. The enum is numeric and carries no [EnumMember], so every one of these codes already reached callers - as a number no constant named, leaving them to match against a literal. That is the same defect 1004 had. Seven members are added, each traced to where the native SDK produces it: UnidentifiedUserLogout 3020 both, Logout on an unidentified profile PaymentPendingError 1050 iOS, ReportTransaction BillingNetworkError 112 Android, the fromBilling mapping WrongAssetType 4104 Android, the flow renderer JsException 4105 Android, the flow renderer NavigatorNotFound 4106 Android, the flow renderer InvalidActionUrl 4107 Android, the flow renderer The last four come out of adapty-ui and reach Unity through FlowViewDidReceiveError, so they touch anything that renders a flow. Reachability is the criterion, and three codes fail it. 1011, 3100 and 10001 are declared by neither native SDK - the same reason they stayed out of the 1004 fix - and the commented-out WrongCallParameter goes with them, since a commented member is an invitation to restore one that does not exist. 1030 is declared by iOS and has a factory, a description and a mapping, but no call site anywhere in the SDK; it stays out too, and the enum is to be re-compared at every native dependency bump rather than padded against a future that may not come. Names follow what the enum already does: a code both platforms share is named after iOS, as WrongParam is against Android's WRONG_PARAMETER; an Android-only code is named after Android. The surface snapshots gain these seven lines on all three platforms and nothing else. The changelog also stops crediting 1004 to both platforms - it is Android-only, iOS does not define it.
StrippingBuild's remark explained managed stripping High as the reason the package ships a link.xml. There is no link.xml - [Preserve] attributes replaced it during the migration - so the sentence pointed at a file that does not exist. The version-bump checklist now says to delete the previous wrapper AAR. No build.gradle in the package references the artifact; Unity picks up any .aar under Plugins/Android as a plugin on its own, so a new one dropped beside the old one puts both into the player.
The floor the package declares and the versions it was tested on were being treated as the same statement, and they are not. 2022.3 is what package.json permits; Unity 6 is where the work happened. Both halves are now stated, and both are measured. On 2022.3.62f3 the install path was run end to end in a clean project with no Newtonsoft: importing the .unitypackage compiles with no errors, the validator names the menu item, Adapty SDK > Install Dependencies writes the OpenUPM registry and installs both packages, and the SDK assembly compiles afterwards. Player builds, device runs and the rest of the acceptance matrix were done on Unity 6, and the wording does not stretch past that. CLAUDE.md carries the same split plus the trap that cost a run: recent 2022.3 builds are Extended LTS and refuse to launch without an Industry or Enterprise licence, so re-verifying this needs a build below that cutoff. Raising the floor to Unity 6 was considered and dropped. Package Manager enforces the declared floor, so it would have taken the registry install away from 2022.3 users to say something that a sentence says just as well.
The migration left a ToJSONNode extension class behind in Models/, where the deletion of Runtime/JSON/ did not reach it. It was the fourth of its kind and the only survivor, while MIGRATION.md already told users the other three were gone. Nothing called it: the refund preference goes through AdaptyJson and the [EnumMember] mapping like every other enum, and the method returned a string, having been patched to compile once JSONNode was gone. Two constructors went with it. Newtonsoft builds AdaptyUIOnboardingMeta and AdaptyOnboarding.OnboardingBuilder through their private constructors and their fields, so the parameterised ones the hand-written parser called have no callers left. Both types now carry the same CS0649 as the rest of the package - those fields really are assigned by reflection alone. AdaptyFlowPaywall.ProductReference is now internal. Its constructor was private and every member already internal, so no instance of it could be obtained or read from outside the SDK; it was public only because 3.x had it as a top-level AdaptyProductReference. IsOneTime and GetIsTrackingPurchases are gone too. Each only forwarded to a neighbour - IsConsumable and the IsTrackingPurchases field - and neither has a caller anywhere in the repository. IsOneTime had been calling itself deprecated in its own summary for several versions, with no attribute to make the compiler say it. The snapshots lose seven public surface lines on each platform and fifteen model lines. Every one of them is one of these members; nothing else moved.
Only the six entry points carried [Obsolete]. Everything they hand back or
take - IAdaptyOnboardingsEventsListener, AdaptyOnboarding, AdaptyUIOnboardingView,
AdaptyUIOnboardingMeta, the AdaptyOnboardingsAnalyticsEvent,
AdaptyOnboardingsStateUpdatedParams and AdaptyOnboardingsInput hierarchies, and
the AdaptyUI.ShowDialog overload for onboarding views - said nothing. Writing a
listener implementation warned only later, at the call that registered it.
Marking them raised 55 CS0618 inside the package, which would have compiled into
the console of everyone who installs the SDK. Two moves bring that to three
without suppressing anything, since a reference from obsolete code to obsolete
code does not warn:
- the parts that are themselves legacy are marked as well: the listener field,
RequireOnboardingsListener, the private DismissOnboardingView overload, and
the two converters that exist to read onboarding payloads;
- the seven onboarding cases move out of Dispatch into
OnLegacyOnboardingMessage. Their bodies are unchanged and Dispatch is still
called from the try in OnMessage, so a throwing listener is contained exactly
as before.
What is left is the honest boundary: the one call into the legacy handler and
the two converters being constructed.
The move was made against no coverage at all - EventDispatchTests only ever
drove did_load_latest_profile - so LegacyOnboardingDispatchTests comes with it.
It runs all seven ids to their own listener method, keeps the three that share a
payload shape in separate cases, and checks the built models, a throwing
listener, broken payloads and a missing listener. Misrouting one call by hand
fails that route's case and nothing else.
Seven public surface lines left the package, and all seven are breaking, so the changelog says so for each of them rather than only for the two members that had callers by name. What has been measured is stated as what it is: the SDK does not call the removed extension. Whether anyone else does is not something this repository can answer. MIGRATION.md gains the replacements for IsOneTime and GetIsTrackingPurchases, names AdaptyRefundPreferenceExtensions alongside the three ToJSONNode classes it already listed as removed, and says that the onboarding deprecation now covers the types too, not just the calls.
The README still promised iOS 9 and Android 4.1. Neither has been true for a while: AdaptyIOSBuildValidator fails a build below an iOS 15.0 deployment target, and the Android wrapper is built against minSdkVersion 21. Unity 2022.3 is now declared as the floor and was missing from the README entirely. Getting started said nothing about the dependencies, which is the one thing a 4.0 install can get wrong. A .unitypackage carries assets only, so Newtonsoft has to be there before the import - while it is absent the SDK assembly is skipped by its define constraint, and the menu item that would install it is unavailable for exactly the same reason. That order is now spelled out. Both LICENSE links pointed at a master branch. The remote has main. CLAUDE.md described OnMessage as the switch; it parses and hands off to Dispatch, and the seven onboarding ids leave through an [Obsolete] method of their own. Written down with the reason, because folding them back into the main switch is an easy edit that multiplies CS0618 by about thirty. The deprecation convention that came out of this release is recorded next to the model convention: mark the whole area rather than its entry points, push the warnings to the boundary instead of silencing them, and know that the surface snapshots record signatures without attributes, so a dropped [Obsolete] fails nothing. The changelog carries the date the beta.2 content settled. Whoever cuts the release moves it if the cut slips.
Nothing in the build compares cross_platform.yaml with the C# that restates it, so the two drift apart silently. This adds the comparison as a skill: a script for the mechanical half, and instructions for the half that needs reading. extract.py flattens the contract's oneOf branches, separates the object types from the request/response envelopes, and diffs property sets, IsRequired flags, platform markers, string enum values and every method and event name against the C# sources. Three of its behaviours are deliberate. It refuses to run when its own walk found implausibly little, because a silent empty parse reports that everything matches. An unmapped contract object is a loud error rather than a skipped line, so a type added to the contract cannot pass unnoticed. And for a contract key with no [DataMember] it prints where that string does occur, converter sites first, since a key supplied by a converter is the usual explanation and not a defect. SKILL.md carries what the script cannot: the converters, which restate contract objects by hand and have no attributes to compare; the write path, which is not the read path; and enforcement, since a required key that is read leniently is a gap even though the property exists. Its three mandatory rules are each there because a trial run failed that way. Re-read every line you cite: one misread line reference produced an entire well-argued finding about a divergence that did not exist. Check a family, not a sample: a rule established from five of six enums missed the sixth, which was the only real finding in that area. Read the whole method: a run that reported one lenient read of a required key missed a second one fifteen lines below it. Five trial runs shaped this. On the first version the runs disagreed with each other more than they agreed and one invented a finding; on this version they stopped inventing, and the strongest run found two defects that none of the others, the test suite, or a hand-written probe had seen.
Two formatting passes over the package, no behaviour in either. `using` directives are sorted, `System` first, which is the order the 31 files outside `Runtime/Models` already used - the sweep brings the models in line rather than introducing anything. Single-line `/// <summary>text</summary>` is expanded to the three-line form the other documentation uses: 41 of them, in the six parameter and builder files where they had collected. The text inside is untouched and unwrapped. Line endings are left as they are in the repository. The reformat arrived as CRLF, and with no `.gitattributes` and no `core.autocrlf` that would have rewritten every line of some fifty files in the history for nothing.
Adapted from the community Unity template at github.com/gitattributes/gitattributes, with one deliberate departure: it routes every binary asset through Git LFS, and this repository does not use LFS - `git lfs ls-files` is empty, and enabling the filter would turn the 29 .png, 22 .unitypackage and 3 .aar files already committed as ordinary blobs into pointers on the next commit that touched them. They are marked `binary` instead, which is what was actually wanted: no end-of-line conversion, no attempt to diff them. `* text=auto` is the rule the repository was missing. Everything text is stored with LF whatever the working tree holds, so a reformat done on one machine can no longer rewrite every line of a file for the next reader - which is exactly what a pass over Runtime/Models produced this week, and what had to be undone by hand before it reached a commit. Two endings are not cosmetic and are pinned against `text=auto`: `gradlew.bat` keeps CRLF for cmd.exe, `gradlew` and the shell scripts keep LF. The Gradle wrapper ships as both halves and each needs its own. Unity's YAML and JSON assets take the template's macros, so a merge of scenes and prefabs still goes through unityyamlmerge.
Whitespace only: the diff is empty under `--ignore-cr-at-eol`, so no logical content moved. The blobs are LF now where they were CRLF, and only `gradlew.bat` checks back out with its original bytes, because .gitattributes pins it to `eol=crlf`. Doing it now rather than leaving it is the point. Git had not reported these as modified, because a stat match lets it skip re-running the filter - so each would have stayed quiet until someone edited it and found 384 lines of end-of-line churn attached to their one-line change. Fifteen files: the twelve ProjectSettings assets, one vendored CharlesProxy source, one stray .meta, and `gradlew.bat`. Worth passing to `git blame --ignore-rev`, or listing in a .git-blame-ignore-revs if the repository grows one.
Three fixes, two of them to rules that were wrong for every file they
matched.
`*.pbxproj text -diff merge=binary` is the right default for a generated
Xcode project, and this repository tracks none. The only two .pbxproj it
has are the Kids Mode fixtures KidsModeTraitTests round-trips between,
where the diff is the whole point: the edit they pin is exact to the tab,
and `Binary files differ` would hide a change to what the test expects.
They get text diff and an ordinary merge back; the general rule stays for
a generated project that may yet be committed.
`ProjectSettings/XRSettings.asset` is the one .asset Unity writes as JSON
rather than YAML - 30 of the 31 tracked start with `%YAML`, this one with
`{`. It was being handed to unityyamlmerge and reported to GitHub as
YAML. It is `unity-json` now, with the merge driver unset by hand, since
the macro sets an eol and a language but does not clear a driver an
earlier rule assigned.
The template is MIT and asks for its notice to be kept, which a file
claiming descent from it should do rather than only name the source.
THIRD_PARTY_NOTICES.md carries it.
`AdaptyKidsModeTrait` was the only iOS-only type in `Editor/` without the prefix its neighbours carry, `AdaptyIOSBuildValidator` and `AdaptyIOSKidsModePostprocessor`. It writes a trait onto a Swift package reference in `project.pbxproj`, so it is iOS-only in the strongest sense - there is nothing for it to do anywhere else. It is not itself a post-processor: it is the text edit, kept free of Unity types so the test project can link and run it. The shim around it keeps its name, which is load-bearing in a way this one's is not - `AdaptyIOSBuildValidator` looks the post-processor up by the string "AdaptySDK.Editor.AdaptyIOSKidsModePostprocessor" to decide whether a build-profile define reached the Editor assemblies. The `<Compile Include>` in AdaptySDK.NextTests names the file directly, so it moves too, and the `.meta` travels with the source to keep the guid.
bdabe2d renamed `AdaptyKidsModeTrait` to `AdaptyIOSKidsModeTrait` and left the two CI comments naming the old one. CI itself was unaffected - the path filter covers `Editor/**`, not the file - but the comment is there to say why that path is in the filter, and it named a type that no longer exists. Missed because the rename swept `*.cs`, `*.csproj` and the documentation and never looked at `.yml`. The search this time was `git grep` over every tracked file with no filter at all, which is what should have been run the first time; the old name appears nowhere now.
The request completion policy was spread across 40 Request.Send call sites in four shapes: 32 wrapped the app's handler identically, one did the same around a list conversion, two answered a round-trip with no app callback at all, four in Obsolete/ wrote the try/catch by hand, and GetOnboarding had no guard. Every site that reported a failure copied the diagnostic string by hand, and thirteen copies had drifted: six named Adapty where the method is on AdaptyUI, three named a signature the handler does not have - one of them AdaptyInstallationDetails for a handler taking AdaptyInstallationStatus - and six differed only by a space. Nothing tied the text to the method, so nothing could catch it. The policy moves into Request. SendRaw is private, so the safe path cannot be bypassed, and Send/SendVoid take [CallerMemberName]: the name is the compiler's, and a drifted copy is no longer possible to write. SendVoid hands the caller on explicitly, or the message would name SendVoid itself. All 40 call sites go through it, including the five in Obsolete/. That GetOnboarding had no guard at all is a fix rather than a cleanup, and the changelog says so. The wrapping itself stays in Callbacks.InvokeSafe, which is still what the 21 event dispatches use; Request supplies only the wording. Two integration tests over the no-op bridge pin the name to the method, and both were shown to fail without the change: dropping the hand-off reports SendVoid, and removing the wrapping reports the app's bare exception. Editor/iOS/Android all green. The wire and the public surface do not move, and no approved snapshot changed.
Every other top-level helper in Runtime/ is named for the SDK - AdaptyJson, AdaptyJsonRequire, AdaptyResponse, AdaptyNoop. Three were not: Request, Callbacks, and ExceptionGetFullMessage. They are now AdaptyRequest, AdaptyCallbacks and AdaptyExceptionExtensions. The rule is about top-level helpers only: nested internal types such as ProductReference, MessageHandler and CallbackHandler keep their names, since the enclosing type is what gives them context. AdaptyExceptionExtensions is the only real extension-method class in the package - it takes `this Exception ex` - so the suffix now says what it is rather than repeating the single method's name. Callbacks.cs moved with its .cs.meta, so the GUID is unchanged and no Unity reference breaks. The csproj globs pick the file up by directory, so neither test project needed an edit. Two things deliberately not renamed: `using UnityEditor.Callbacks` in the Kids Mode postprocessor is Unity's namespace, and the Request.*.swift paths in the iOS reference skill name files in AdaptySDK-iOS. CLAUDE.md also had two claims that the previous commit made false: the deprecated onboarding requests no longer keep hand-written wrappers, and the call that had none is fixed. Only the seven legacy onboarding events still wrap by hand, and that is where the note now points. Editor/iOS/Android all green.
Two leftovers from the rename commit. The key-files list still called the transport `Request`, so the document contradicted its own line 43 and pointed a maintainer at a type that no longer exists. My leftover search had matched `Request.Send` rather than the bare word, which is how a prose mention survived - the same too-narrow-search mistake as the last two renames. The paragraph on events also claimed every call into the app goes through AdaptyCallbacks.InvokeSafe and then, two sentences later, excepted the seven legacy onboarding events that wrap by hand. The claim is about the live API, and now says so.
Every other top-level helper in Runtime/ has a file named after it - AdaptyCallbacks, AdaptyJson, AdaptyJsonRequire, AdaptyResponse. The transport sat at the bottom of Adapty.cs for no reason beyond history. Files here do hold more than one type, but only where the types belong together: a polymorphic root with its subclasses, a bridge with its callback action. The transport and the public API surface are not that. Nothing about the class changed - the diff is a cut and a paste, plus the summary it had never had. Adapty.cs drops from 935 lines to 830, and loses two usings the move made dead: System.Runtime.CompilerServices, which only [CallerMemberName] needed, and the _Adapty alias block, which only the raw send used. The alias moved with it, so the package still declares it in three files and still has 26 #if - both counts CLAUDE.md states, and both measured against HEAD rather than assumed. CLAUDE.md said the class was at the bottom of Adapty.cs, in two places. It now points at the new file. The .cs.meta carries a fresh GUID, checked against every tracked .meta for collision. git diff --check flags three trailing spaces in it; that is Unity's own format, byte for byte what it writes for every script, and correcting it would only make Unity write it back. Editor/iOS/Android all green.
Checked every path the skill names against the 4.0.2 checkout, not only the two that had been reported. Eight were wrong: - the dependency file moved to Packages/com.adapty.unity-sdk/Runtime/Editor/ when the project went UPM, and it is a <swiftPackage> now, not an <iosPod>; - Sources/Environment/ is spelled Envoriment/ upstream, so the documented glob silently finds nothing - the map now says so rather than repeating the correct spelling; - Sources.KidsMode/ does not exist. Kids Mode is a Package.swift trait, and the map now names the three files its #if guards; - Adapty.podspec is gone - SwiftPM only, which is why the Unity side declares a Swift package; - Request.GetPaywall.swift and Sources/Adapty+MakePurchase.swift are both used as worked examples and neither exists: the first is Request.GetPaywallProducts.swift, the second sits under StoreKit/; - only three Adapty+*.swift are at the root, not the whole public API; - Sources.Codable/, Examples/ and scripts/ were missing from the map, as were six Sources/ directories including Errors/, which is what the changelog work on error codes had to read. The clone command keeps the canonical URL, with a note to use the machine's host alias where SSH is configured that way. The map now states the tag it was verified against, since the layout moves between majors and a map that lies costs more than no map. Same check run over contract-conformance: its paths are all live.
The platform callback transport was registered by the four listener setters and by nothing else, so an app that subscribed to no events got no completion handler called at all - Activate included - on either platform, and every iOS request additionally leaked the GCHandle meant to carry its reply. Present since 3.x. The demo in this repository sets all four listeners before it activates, which is why nothing here ever hit it. Adapty.InitializeTransport now does it at BeforeSceneLoad. The stage covers the whole MonoBehaviour lifecycle, which is what the SDK guarantees, and the environment is fully up there - this one crosses into JNI. An app reaching the SDK from a hook it scheduled earlier is outside that guarantee, decided rather than overlooked, and that is why there is no second registration inside the platform Invoke. Registration therefore has one call site. The five in the setters are gone, the deprecated onboarding one included - internal call removal, no name, signature or type touched - and it took the last user of that file's _AdaptyCallbackAction alias with it. The package is down to 25 #if from 26, and the census in CLAUDE.md follows. TheTransportIsRegisteredBeforeTheFirstScene pins the stage rather than the exact value: earlier is a legitimate change, later is this bug again. Both mutations were checked to fail it - the stage moved to AfterSceneLoad, and the attribute dropped. Not verified on a device: that Unity calls the hook, and that both bridges deliver a completion afterwards with no listener ever set. No desktop test can see either.
One commit rather than several: the mirrors of every changelog entry live in a single line of package.json (_upm.changelog), so splitting would either divide that line dishonestly or leave it out of the commits it belongs to. Three defects, one breaking rename, and a set of documentation corrections. Defects: - UpdateAttribution(IReadOnlyDictionary, ..) encoded its argument before it could build a request, which is outside the transport's own guard, so a reference loop or a throwing getter in the provider's graph was thrown at the caller while every other method reports an error. It was the only such call site in the runtime. AdaptyRequest.FailEncoding now reports it as EncodingFailed through the same callback policy, and SendRaw and it share one EncodingFailed(method, exception) so the wording cannot drift. - Adapty SDK > Install Dependencies read Copies(..).FirstOrDefault() and never counted. The order of AppDomain.GetAssemblies() is not specified, so a project with two copies of Newtonsoft.Json - a state the SDK's own validator reports as an error - was told its dependencies were complete or sent to fix them depending on which copy came back first. The list is materialized once now, and the duplicate wording moved to AdaptyDependencies.DuplicateMessage, shared with the validator. - AdaptyConfiguration.Builder.ToString() omitted GoogleEnablePendingPrepaidPlans, the one member it carries that was missing, so two builders differing only in it printed identically. Breaking: - Adapty.GetLoglevel is Adapty.GetLogLevel. The typo was in the v3 surface too, out of step with its own SetLogLevel and with get_log_level, the name the contract gives the operation. Same signature, same wire method. The three approved surface snapshots are regenerated and remain byte-identical to each other. Documentation, each verified against the thing it describes: - AdaptyErrorCode.EncodingFailed was called iOS only. It is raised in managed code before the platform alias is reached, so it arrives on either platform; DecodingFailed is the same, and the enum's own remarks claimed every value is a native code. - The cancellation_reason lists were missing `upgraded`. AdaptySDK-iOS 4.0.2 lists seven values where both models listed six. The lists now match the native order, and say the set is open - the contract types the field as a string. - Seven members of the shared profile models were documented as App Store or Apple only, although the contract defines them outside any platform branch and they sit next to Store, whose own documentation lists play_store. - The comment above ProductPurchaseParametersForRequest described the wire key as the store's product id. The contract says adapty_product_id, which is what the code sends; the comment invited a change that would have sent a well-formed request matching no product. - The changelog said PaymentPendingError (1050) comes from ReportTransaction and that it always arrived. Its only throw site at the pinned tag is reportPurchaseResult(StoreKit.Product.PurchaseResult), which the plugin never calls - it calls reportTransaction(transactionId:withVariationId:), which has no pending branch. This contradicted the member's own docs. - MIGRATION.md told migrating apps to implement the three new flow callbacks as no-ops because flows "do not emit these events yet". All three are emitted by the pinned native SDK and routed by the dispatcher. A permission request left unanswered stays pending until the view is dismissed, and an empty app-review handler is worse than registering none, since the SDK makes that call itself when no handler is set. The guide also contradicted itself, having said five lines earlier that a permission must be answered exactly once. - MIGRATION.md's flow.Paywalls example assigned to IList<T>, which does not compile against the IReadOnlyList<T> it returns, and its dictionary copy used a Dictionary constructor that only exists from netstandard2.1 - measured, it fails on netstandard2.0 and builds on net8.0. Both replaced; every other example checked against the approved surface. - Two IntelliSense links pointed at the wrong section of the right page, and the README quickstart put its query string inside the fragment. All three targets were checked before being changed. Known issue, recorded rather than fixed: custom color and linear gradient assets are not rendered on iOS. The pinned AdaptySDK-iOS 4.0.2 substitutes a transparent color and an empty gradient for whatever it receives, identically in 4.0.3 and 4.1.0, so there is no version to move the pin to and nothing to change here - the Unity side serializes the real values. Whether Android is affected is not established; the wording says so. Two new tests, each confirmed to fail against the unfixed code: UpdateAttributionReportsAGraphItCannotEncode and TheConfigurationBuilderDescribesEveryMemberItCarries, the latter checking the builder's description names every member it carries without pinning a format the type says is not a contract. Test matrix green: 224 editor, 158 iOS, 158 Android. The Editor assembly is outside every .NET project here, so it was verified in a live Editor instead (6000.4.5f1): after a forced refresh and recompile, zero errors, and reflection on the loaded assemblies confirms the new members are the ones in them. Device behaviour is unverified, as before.
Running the suite with ADAPTY_KIDS_MODE set failed three tests and always had: on iOS the define forces apple_idfa_collection_disabled, the approved configuration requests hold it as false, and no approved form of the Kids Mode output existed anywhere. So the one thing the define changes that this layer can observe - and the one thing a Kids Category build cannot afford to get wrong - was pinned by nothing, while the combination itself was red for anyone who tried it. The three configuration snapshots now have a second approved form under `-kids`, picked by RequestParityTests.Configured. Only those three: the define moves exactly one line of exactly those requests, confirmed by diffing each against its iOS counterpart, so a fourth snapshot ever needing a `-kids` form would mean the blast radius changed and is worth reading before approving. CI gets a fourth leg. The matrix becomes an include list so the leg can carry a name that is not its define set - "UNITY_IOS;ADAPTY_KIDS_MODE" is not usable as an artifact name, and the ';' needs MSBuild's %3B escape or the shell takes it for the end of the command. Verified the leg is not vacuous: with the forcing block disabled the three tests fail under it, and pass with it. All four legs green - 224 editor, 158 iOS, 158 Android, 158 iOS+Kids. CLAUDE.md described the matrix as three; it says four now, and says which snapshots the define touches.
The file is instructions, so a stale sentence in it is worse than a missing one - it sends the next change down a path that no longer exists. This is the pass that was deferred while the package audit ran. CLAUDE.md becomes a pointer, not a copy and not a symlink. Two copies of a rule drift apart and the one you happened to open is then as likely to be the stale one; a symlink is easy to follow without noticing and easy to break without noticing. What the pass actually checked, and what it found: - Every numeric claim that can be counted. 25 `#if` in the package with the five-kind split, six partial types, four abstract roots, seven onboarding ids, the three `UNITY_IOS || UNITY_EDITOR` guards, four listener fields reset and four asserted by the Play Mode test, the two string enums that keep `Unknown`, five groups overloading on the first argument's type, the three `Obsolete` compile lines in each of the two csprojs. All hold. - Every version it names against the file that declares it: iOS 4.0.2, Android 4.0.1, EDM 1.2.188, Newtonsoft 3.2.2, SDKVersion and package.json both 4.0.0-beta.2, contract $id 4.0.2. All hold. - Two claims about what is deliberately absent: `unknownTransactionId` (1030) is still not in the enum, `obfuscated_profile_id` is still in the contract and implemented by nobody. Both hold. - All 132 backticked identifiers, mechanically. Two were wrong. - All 60 paths. None were wrong - the sixteen that did not resolve are bare filenames in prose or paths inside the AdaptySDK-iOS checkout. The two real defects: - `JsonRequire` is `AdaptyJsonRequire`. Anyone grepping the name the file gave them found nothing. The same wrong name was in the contract-conformance skill and is fixed there too. The `Serialization/` listing was also short by two files, one of them `AdaptyResponse` - the reply side, and the only place `DecodingFailed` is raised, which the error-code section three screens down describes without naming where it lives. - `AdaptyRequest.FailEncoding` was missing from the transport description, which still said the two entry points were the whole of that surface. One day old, from the audit fixes. It now says what the third member is for and why a second caller wants thinking about first. Test matrix green on all four legs.
The sentence claimed a bare `;` would be read by the shell as the end of the command. Both the workflow and the form AGENTS.md documents quote the argument, so the shell never sees the separator. The reason `%3B` is there at all is MSBuild's own parsing of a `-p:` value, and that is what the comment now says.
The deploy tooling was the object of this pass; the installer defect is what the pass turned up, and the two share the version strings. Install Dependencies judged External Dependency Manager by whether a Google.VersionHandler assembly was loaded. v3 declared EDM 1.2.187 and v4 needs 1.2.188, so a project arriving from v3 kept the old one and was told "Every dependency is already installed" - while the iOS build resolved through the version that gets the Xcode project path wrong for the Swift project type. The requirement was already written down in MIGRATION.md and enforced by nothing. A package-managed copy below the floor now goes into the same AddAndRemove call, which Package Manager treats as an upgrade. A copy it does not describe - the one Google ships as its own .unitypackage under Assets/ - is warned about instead: adding the package over it would leave two, and the version cannot be established anyway. Measured in the Editor rather than assumed: every 1.2.x build of Google.VersionHandler reports assembly version 1.2.0.0, so PackageInfo is the only thing that can tell 1.2.187 from 1.2.188. Two loaded copies count as unmanaged for the same reason the Newtonsoft branch materializes its list - GetAssemblies() has no specified order, so picking one would decide the same project differently run to run. The decision moves to AdaptyDependencyPlan, which references nothing from Unity, so it is testable the way AdaptyManifest already is. DependencyPlanTests pins the upgrade from 1.2.187 among fourteen cases; reverting the check to presence-only fails three of them. On the deploy tooling itself: - build_unitypackage.sh wrote the staging manifest with the Newtonsoft version spelled out, a third copy of it that nothing compared to the package. It reads package.json now and refuses to run if the dependency is not there. The export is byte-identical either way. - PackageManifestTests pins what is left: Adapty.SDKVersion and the installer's constants against package.json. Nothing compared them, and the tag and artifact name both come from the manifest - so a forgotten SDKVersion ships under a correct-looking name. - AGENTS.md described build_unitypackage.sh and claimed a dev mode that "keeps Library/", which it never did. release_unitypackage.sh - the one that commits, tags, pushes and publishes - was not mentioned at all, nor was Releases/. Releases/ is also not the complete history it looked like: 4.0.0-beta.1 was tagged without an artifact. Four places still described the installer's old rule, including the two lines in MIGRATION.md that the affected reader is the most likely to reach. All of them now say the one rule: missing dependencies are installed, a package-managed EDM below 1.2.188 is upgraded, an unmanaged one is left alone with a warning. Verified: the four-leg matrix at 240/174/174/174, the package's Editor assembly compiling in a live Editor, and the plan invoked there returning the upgrade for 1.2.187. The 1.2.187 iOS build itself was not reproduced - the floor is taken from MIGRATION.md and the EDM changelog.
The guide is what a v3 user reads, so "does it cover everything" had to be answered mechanically rather than by rereading it. The public surface of tag 3.17.0 was built with the same code that renders the current approved snapshot - the v3 assembly swapped in for AdaptySDK.Surface.dll, then PublicSurfaceTests and its received file - so both sides come out of one formatter. 837 members against 652. Then all 105 commits since 3.17.0, split into "fixes code that shipped in v3" and "fixes a break made during the migration". Five defects that shipped in 3.17.0 were in neither document. Each one was confirmed by reading the sources at the tag, not the commit that fixed it: - SetServerCluster did nothing. ServerCluster is the one builder field AdaptyConfiguration's constructor does not copy, so server_cluster was never sent and an app that chose EU or CN ran on the default cluster anyway. This one has a consequence on upgrade: the traffic moves to the region that was asked for and never used. - AdaptyPlacementFetchPolicy.Default was null. It aliases ReloadRevalidatingCacheData but is declared above it, and static field initializers run in declaration order. - UpdateAttribution dropped bool and DateTime. The dictionary serializer had branches for strings, numbers, nested dictionaries, lists and null. - AdaptyCustomerIdentity.IsEmpty never returned true. IosAppAccountToken is a non-nullable Guid compared against null. - A partially filled onboarding date_picker threw. The helpers for the optional day/month/year cast their nullable straight to int. All five are in CHANGELOG.md and _upm.changelog, 51 entries each. None are in the guide: a fixed bug asks nothing of the reader. The guide itself goes from 447 lines to 279, restructured as six sections in the order the work happens - prerequisites, the flow rename, listeners, compile errors, silent runtime changes, optional. What came out was release rationale that the changelog already carries: the verification history, the define constraint mechanics, the native enum evidence, the subclassable-type inventory, the swift-tools-version explanation, the full deprecated-onboarding list. Three bug fixes left the behaviour section and came back under "remove workarounds you no longer need", where they name an action instead of a defect. Nothing actionable was lost, checked the same mechanical way: of the 60 removed and changed identifiers in the surface diff, 52 are named in the guide and 8 are covered by rules that name their owning type. The four *Extensions classes were caught by that check after the first cut collapsed them into a phrase, and put back - someone reading CS0117 greps for the name.
Five gaps, all of them things a first-time reader hits before writing any code, and one of them costs a broken build: - Nothing said which Xcode artifact to open. External Dependency Manager wires Pods_UnityFramework into the Unity target, so building Unity-iPhone.xcodeproj fails with `ld: framework 'Pods_UnityFramework' not found` while the workspace builds fine. The migration guide had this; a new user has no reason to read a migration guide. - Flows were not mentioned at all. The README advertised paywalls and onboardings, and in v4 both are fetched with GetFlow and shown with CreateFlowView, while the separate onboarding API is [Obsolete] - so someone arriving for onboardings started a new integration against the deprecated path, with the warnings to match. - "Add the package by Git URL" named no URL. package.json is not at the repository root, so without ?path=/Packages/com.adapty.unity-sdk Unity looks for a manifest in the root and installs nothing, with no way to guess the missing piece. - External Dependency Manager was described as resolving "the native SDKs". AdaptySDKDependencies.xml holds one remoteSwiftPackage and nothing else; Android's dependencies are in the bundled .androidlib's build.gradle and never go through EDM. An Android-only project was being told to install a package it does not need. - No route to the changelog, which is where the known issues live - iOS not rendering custom color and gradient assets, among them. Someone would have debugged their own configuration against a pinned native SDK defect. Two more that came out of the same read: - The .unitypackage instructions explained the prerequisites but not where to get the file. Now they link Releases. - The README promised the menu item upgrades an old External Dependency Manager, full stop. That is only true of a package-managed copy: one imported from Google's own .unitypackage has no version Package Manager can read, so the installer warns and leaves it, which is what the code committed earlier today actually does. "Adapty SDK does everything with a single line of code" becomes "handles them all through one API". No call does all of that, and the minimum integration is Activate plus a fetch plus a purchase, with listeners for some of it. The line is shared with the other Adapty SDK READMEs, so this one now differs from them - worth propagating rather than reverting. MIGRATION.md is renamed to MIGRATION-v3.17-to-v4.0.md so the next incompatible release adds a file instead of overwriting this one. Now is the only cheap moment: the file is on no published branch, so nothing outside points at it, while after the release the blob/main URLs in the changelog and in _upm.changelog - the copy Package Manager shows - would break. All four references updated with it.
The README sent the reader to the whole changelog and let them find it. The limitation it hides is one a reader can walk straight into from this file: custom color and linear gradient assets reach an iOS flow view as a transparent color and an empty gradient, because the pinned native SDK substitutes those for whatever it receives. 4.0.3 and 4.1.0 do the same, so the pin has nowhere to go. The entry names the effect, the API path it happens on, and what closing it waits on — a native iOS release and acceptance after it — so the section reads as what is still open rather than a warning to be careful. Scoped to custom assets the app passes at view creation. Colors configured in the no-code builder travel a different path and nothing here claims otherwise. Placed at the end of the file, after License, where it was asked for. The full text stays in the changelog and in the remarks on the two factories; this is the third place that says it, and the only one a new integrator reads first.
…r claim A .unitypackage never removes files. Importing 4.0 over an existing Assets/AdaptySDK keeps the folder and asmdef GUIDs, so the 62 sources this release drops stay behind and compile into the same assembly as the new ones: 35 of them declare a half of a type the new sources also declare, and the rest reach for constructors and a SimpleJSON namespace that are gone. Nothing said so. The failure is also badly timed - the assembly is gated on Newtonsoft, so the errors surface only once Newtonsoft is installed, which is after the documented steps have all succeeded. The same paragraph got the installer wrong in the other direction. The Editor assembly carries no define constraint and references no Runtime type, deliberately, so "Adapty SDK > Install Dependencies" is available with Newtonsoft missing - AdaptyNewtonsoftValidator prints that very instruction in that very state. What makes it unreachable is the project's own scripts failing to compile, which is what the migration guide already said and the README, changelog and _upm copy did not. Four smaller gaps in the migration guide, each a member a v3 caller can have written: - AdaptyUIPaywallView.PaywallVariationId is AdaptyUIFlowView.VariationId. The guide mapped the type and not the member, so the only signal was CS1061. - AdaptyProfileParameters.CustomAttributes shared a row with the parameter objects, which take an IReadOnlyDictionary and copy it. It takes nothing - it is a view over the builder's storage, and the writers are SetCustomStringAttribute, SetCustomDoubleAttribute and RemoveCustomAttribute. - GetIsTrackingPurchases returned bool; the field it wrapped is bool?, so the replacement needs ?? false to keep the old expression's type. - builder.IdfaCollectionDisabled, the property beside the renamed method, was removed unmentioned.
Canonical() parsed with JToken.Parse, whose reader defaults to DateParseHandling.DateTime, so every ISO string in a request was turned into a DateTime and printed back in Newtonsoft's own form before being compared. The approved files recorded that form, not the SDK's: AdaptyConverterDateTime emits yyyy-MM-ddTHH:mm:ss.fffZ, and transport-create-flow-view.editor.approved.txt held "2026-07-30T10:00:00Z" - the milliseconds were lost on the way into the snapshot. Dropping .fff from the converter, or emitting +00:00 instead of Z, would therefore have moved no approved file at all. Load through a JsonTextReader with DateParseHandling.None instead, which is what AdaptyJson.ParseDocument already does and for the same reason. One approved line moves, and it moves to what goes over the bridge. Mathf.RoundToInt in the stubs rounded midpoints away from zero; Unity's is (int)Math.Round(f), which rounds to even. AdaptyCustomAssetPath.ColorToHex is built entirely on it, so a channel landing exactly on .5 would have been approved as a value no real player sends. No sample colour is on a midpoint today, so nothing moves - the stub is now right before it matters.
AdaptyConverterDateTime has two read branches, and they disagreed about a string with no Z and no offset. The pre-parsed branch treats an unspecified kind as UTC and converts it; the string branch called DateTime.Parse with the default styles, which leaves such a value Unspecified and unconverted - so the instant came back shifted by the device's offset, and the kind contradicted the convention the type documents. AssumeUniversal settles it as UTC and AdjustToUniversal resolves the shapes that do carry a designator, with ToLocalTime putting both back on the contract's local side. Nothing that arrives today changes: the contract's format always carries a zone and neither native SDK omits one, which is also why no approved snapshot moves. Pinned by ADateCarryingNoZoneIsReadAsUtc, beside the test that covers the other branch.
Two orderings around registerMessageHandler, both reachable only if the Looper check fails, and both leaving no way back: - The Java side assigned messageHandler before checking, so a throw left the wrapper holding a listener it could never deliver to. It is assigned last now. - The C# side set m_IsInitialized before CallStatic, so a throw left the flag standing. That method has exactly one caller, a [RuntimeInitializeOnLoadMethod], so nothing would have tried again for the life of the process. The flag is set once registration has returned. The exception's own text named Adapty.SetEventListener as the caller, which stopped being true when registration moved out of the listener setters and into Adapty.InitializeTransport. It names that instead, and says when it runs. Neither ordering fires on a supported path - a device run confirms Unity's scripting thread has a prepared Looper - so this is the guard being correct rather than a defect being repaired. The AAR is rebuilt from these sources; javap confirms the athrow precedes the putstatic. Also drops a stale count from the AOT probe's notes: no reading of Runtime/ yields 155 readonly fields, and the sentence did not need one.
main carries the previous major until a release merges into it, and 4.0.0-beta.1 shows a prerelease can be tagged without ever reaching it. Two links were written as though that gap did not exist: - The Package Manager install URL in README.md had no ref, so it resolves to the default branch. Anyone following it during the beta installs 3.17 and is told nothing - there is no error, just the wrong major. It carries #4.0.0-beta.2 now. - The CHANGELOG section and its _upm.changelog copy reached MIGRATION-v3.17-to-v4.0.md through blob/main, where that file does not exist. Package Manager shows _upm.changelog to everyone who installs the package, so this 404 is on the path of every beta user. Both use blob/4.0.0-beta.2. A section keeps the tag it was written for, so old ones are never re-pinned. The section's date said 2026-08-08 while the branch had carried on to the 15th, which is what AGENTS.md means by setting it on the day of the cut. Set to today; it moves again if the cut does. Version Bumping gains a seventh step for the two pinned URLs, since nothing else would catch them.
Two delegate surfaces hand app code a callback that sends a request when invoked: the permission respond of IAdaptyUISystemRequestsHandler, and the four observer-mode reports. The app invokes them where its own callbacks arrive - an OS permission callback, a billing thread - and on Android the send is JNI, which the invoking thread must be attached to the JVM to enter. Whether it is attached depends on the Editor version, measured on a device (Nothing A001, Android 16): a raw AndroidJavaClass call from a C# worker thread throws on 2022.3.62f3 - "Field SDK_INT or type signature not found", the unattached-thread failure mode - and succeeds on 6000.4.5f1, which attaches on demand. 2022.3 is the package's declared floor, so the throw is a supported configuration: the answer leaves as an exception into the app's thread, and the flow stays blocked waiting for a reply that never went out. InitializeTransport now captures Unity's SynchronizationContext and the main thread id - the same stage that registers the bridge, for the same boundary - and the two senders hop through it when invoked off the main thread. On the main thread, and on a host that never captured a context, the send stays inline, which is exactly where every call was made before. Acceptance on the same device, Unity 6000.4.5f1 demo build: respond invoked from a worker thread returns without throwing, and a probe post through the same context drains 28ms later on the main thread with nothing logged in between - the queued send ran there, and CallStatic is synchronous into the wrapper, so a clean return is the answer arriving. Two dispatcher tests pin the same thing on the no-op bridge: both delegates driven from a worker thread against a pumping context, the request reaching the transport only after the pump runs, and on the pump's thread. The two interfaces now state the guarantee instead of leaving it to luck: safe to invoke from any thread, the SDK sends from the Unity main thread.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the bundled SimpleJSON with Newtonsoft.Json across the whole JSON layer, and carries
everything the migration surfaced: behavioral fixes that had been invisible without tests, a test
suite that pins the wire format per platform, iOS Kids Mode, a dependency installer for
.unitypackageusers, and the 4.0 documentation set. Version:4.0.0-beta.2.The migration itself
com.unity.nuget.newtonsoft-json3.2.2. While the package is absent,the SDK assembly is skipped via a define constraint instead of failing to compile, and the Editor
assembly (which carries no constraint, deliberately) reports the state and installs the fix.
*+JSON.csparsers and theAdaptySDK.SimpleJSONnamespace are gone. Modelsdeclare their contract with
System.Runtime.Serializationattributes only; Newtonsoft specificslive in
Runtime/Serialization/(one entry point, a contract resolver, five converters).(flows rename,
I-prefixed listener interfaces, read-only collections, sealed models, strictstring enums without
Unknown, flattenedAdaptyInstallationStatus) are listed in the changelog.Fixed along the way (the changelog has the full list with scope and since-when)
ServerClusterselected through the configuration builder was never sent — EU/CN did nothing inv3 and takes effect now.
setters: an app that never subscribed to events got no completion handler called at all, on
either platform, since 3.x.
SetBirthdaysent unpadded dates (1990-3-7); dates reach the app as local time again;AdaptyPurchaseResult.ToString()no longer throws on pending/cancelled results;AdaptyPlacementFetchPolicy.Defaultis no longer null;UpdateAttributionno longer dropsboolandDateTimevalues; contract-required offer fields are enforced at read time.responddelegate and the observer-mode report callbacks are now safe to invokefrom any thread: the SDK sends the request they produce from the Unity main thread. Measured on
a device: a C# worker thread cannot enter JNI on Unity 2022.3 (the declared floor), so answering
from one threw and left the flow blocked; Unity 6 attaches on demand and happened to work.
Added
ADAPTY_KIDS_MODEdefine enables theKidsModetrait on theAdaptySDK-iOS Swift package (IDFA/AdSupport/ATT compiled out) and forces
apple_idfa_collection_disabledin the runtime configuration.its OpenUPM registry) for
.unitypackageinstalls, upgrades an EDM older than 1.2.188, andrefuses states it cannot fix (two Newtonsoft copies, a standalone DLL under
Assets/).AdaptyUICreateFlowViewParameters.Locale,AdaptyUIFlowView.Locale,EnableSafeAreaPaddings(Android), restored/newAdaptyErrorCodemembers traced to nativethrow sites.
Tests and CI
tests/) links the SDK sources into a plain library — no Editor needed — andruns a four-leg matrix: editor,
UNITY_IOS,UNITY_ANDROID,UNITY_IOS + ADAPTY_KIDS_MODE.Approved snapshots pin every request and response shape per platform, the public surface, and
the stripping guarantees ([Preserve] coverage). CI runs the same matrix.
IL2CPP assumptions on a stripped player.
Versions and pins
adaptyandroidwrapper/sources),cross-platform contract 4.0.2 (byte-identical to the canonical copy in AdaptySDK-iOS).
package.json), Xcode 26 for iOS builds (SwiftPMtools-version 6.2). Install path verified end-to-end on 2022.3.62f3; everything else on Unity 6.
Known issue
the values it receives, same in 4.0.3/4.1.0, so there is no version to move the pin to.
Documented in the changelog and README.
Verification
verified end to end; the JNI premise measured on both 2022.3.62f3 and 6000.4.5f1.
4.0.0-beta.2tag, which the release scriptcreates — they 404 until the release is published, by design.