Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .claude/skills/contract-conformance/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@
"CustomerIdentityParameters": "AdaptyCustomerIdentity",
"AdaptyPaywallProduct.Response": "AdaptyPaywallProduct",
"AdaptyPaywallProduct.Request": "AdaptyPaywallProductRequest",
"AdaptyPaywallProduct.Subscription": "AdaptySubscription",
# Renamed from AdaptyPaywallProduct.Subscription in contract 4.1.0, now shared with the
# promoted product.
"AdaptyProduct.Subscription": "AdaptySubscription",
"AdaptyPromotedProduct.Response": "AdaptyPromotedProduct",
"AdaptyPromotedProduct.Request": "AdaptyPromotedProductRequest",
"AdaptySubscriptionOffer.Phase": "AdaptySubscriptionPhase",
"AdaptyUI.FlowView": "AdaptyUIFlowView",
"AdaptyUI.OnboardingView": "AdaptyUIOnboardingView",
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Not every `[RuntimeInitializeOnLoadMethod]` here is a reset. `Adapty.InitializeT

### Platform conditionals

There are 25 `#if` in the package, and each belongs to one of five kinds. **Compilation boundary**, 8 — a native symbol exists only there: the `_Adapty` and `_AdaptyCallbackAction` aliases, everything under `Plugins/iOS` and `Plugins/Android`, the Kids Mode post-processor. **Wire contract**, 11 — the contract itself differs: a `[DataMember]` the schema marks platform-only, `offer_tags` read on Android alone, the offer id required on one more branch there. **Public API behaviour**, 3 — the iOS-only methods that on an Android device report `null`, meaning success. Their guard is `UNITY_IOS || UNITY_EDITOR` rather than `UNITY_IOS && !UNITY_EDITOR`, so the Editor reaches the no-op bridge like every other method instead of taking that branch; three `TransportTests` cases pin it. **Where a file actually is**, 2 — a `StreamingAssets` path is a different location on each platform, and the two places that resolve one do **not** agree about the Editor. `AdaptyCustomAssetPath.Resolve` has an `#else` and hands the path back unchanged. `Adapty.SetFallback` has none, so in the Editor it sends no `path` key at all — `transport-set-fallback.editor.approved.txt` is the whole request, `{"method": "set_fallback"}`. Nothing is lost by that: the Editor call reaches the no-op bridge and comes back with the not-supported error, having never needed a path. It is not an omission to repair. **The Kids Mode define**, 1 — `AdaptyConfiguration` forces `apple_idfa_collection_disabled` under `ADAPTY_KIDS_MODE && UNITY_IOS`, because the trait has compiled IDFA out of the binary and the request has to say so.
There are 26 `#if` in the package, and each belongs to one of five kinds. **Compilation boundary**, 8 — a native symbol exists only there: the `_Adapty` and `_AdaptyCallbackAction` aliases, everything under `Plugins/iOS` and `Plugins/Android`, the Kids Mode post-processor. **Wire contract**, 12 — the contract itself differs: a `[DataMember]` the schema marks platform-only (`is_family_shareable` on both product responses), `offer_tags` read on Android alone, the offer id required on one more branch there. **Public API behaviour**, 3 — the iOS-only methods that on an Android device report `null`, meaning success. Their guard is `UNITY_IOS || UNITY_EDITOR` rather than `UNITY_IOS && !UNITY_EDITOR`, so the Editor reaches the no-op bridge like every other method instead of taking that branch; three `TransportTests` cases pin it. **Where a file actually is**, 2 — a `StreamingAssets` path is a different location on each platform, and the two places that resolve one do **not** agree about the Editor. `AdaptyCustomAssetPath.Resolve` has an `#else` and hands the path back unchanged. `Adapty.SetFallback` has none, so in the Editor it sends no `path` key at all — `transport-set-fallback.editor.approved.txt` is the whole request, `{"method": "set_fallback"}`. Nothing is lost by that: the Editor call reaches the no-op bridge and comes back with the not-supported error, having never needed a path. It is not an omission to repair. **The Kids Mode define**, 1 — `AdaptyConfiguration` forces `apple_idfa_collection_disabled` under `ADAPTY_KIDS_MODE && UNITY_IOS`, because the trait has compiled IDFA out of the binary and the request has to say so.

The last two kinds are not platform questions the layer above could have answered, which is why they are here rather than folded away. Nothing else qualifies. A constructor must not re-decide by define what the layer above already decided: `AdaptySubscriptionOffer` used to null `OfferTags` off Android although its only caller, the converter, passes null there anyway — the approved snapshots did not move when it went, which is what redundant means.

Expand Down Expand Up @@ -162,6 +162,6 @@ When releasing a new version, update:

Steps 1, 2 and 6 are the ones a release can get wrong without anything noticing — the tag and the artifact name are both derived from `package.json`, so a forgotten `SDKVersion` ships under a correct-looking name. `PackageManifestTests` compares all three against the manifest and fails the suite instead.

The `contract-conformance` skill compares the contract with the C# that restates it; run it whenever step 3 or step 4 moves. One key is worth naming here because a conformance run will keep reporting it: `CustomerIdentityParameters.obfuscated_profile_id` is declared in the contract, Android only, and implemented by nobody — not by this SDK, not by AdaptySDK-Android 4.0.1, and iOS has no such field at all. It was left unimplemented deliberately rather than guessed at. **Re-check it on every native bump**: the moment Android starts reading it, the Unity side has to carry it too, and that is a public API change to `AdaptyCustomerIdentity`, so it wants to land in a release that expects one.
The `contract-conformance` skill compares the contract with the C# that restates it; run it whenever step 3 or step 4 moves. One key is worth naming here because a conformance run will keep reporting it: `CustomerIdentityParameters.obfuscated_profile_id` is declared in the contract, Android only, and implemented by nobody — not by this SDK, not by AdaptySDK-Android 4.1.0, and iOS has no such field at all. It was left unimplemented deliberately rather than guessed at. **Re-check it on every native bump**: the moment Android starts reading it, the Unity side has to carry it too, and that is a public API change to `AdaptyCustomerIdentity`, so it wants to land in a release that expects one.

`AdaptyErrorCode.unknownTransactionId` (1030) is the second such item, decided the same way. iOS 4.0.2 declares it, this enum does not name it, and no member is added — because nothing produces it: across the whole of `Sources/` at that tag the identifier appears only in `Sources/Errors/`, as a declaration, a description and a factory method with no call site. A code that cannot be raised needs no constant. **Re-check on every native bump** by grepping the tag for a throw site; the day one exists, the member is owed.
2 changes: 2 additions & 0 deletions Assets/Editor/AdaptyPlayModeSeed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ private sealed class Sink : IAdaptyEventListener
{
public void OnLoadLatestProfile(AdaptyProfile profile) { }

public void OnReceivePromotedPurchase(AdaptyPromotedProduct product) { }

public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { }

public void OnInstallationDetailsFail(AdaptyError error) { }
Expand Down
6 changes: 3 additions & 3 deletions Assets/Scenes/SampleScene.unity
Original file line number Diff line number Diff line change
Expand Up @@ -11528,7 +11528,7 @@ GameObject:
- component: {fileID: 4323543924197680724}
- component: {fileID: 911211715495459256}
m_Layer: 5
m_Name: Button Update Attribution
m_Name: Button Update External Attribution
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
Expand Down Expand Up @@ -12917,7 +12917,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Update Attribution
m_text: Update External Attribution
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
Expand Down Expand Up @@ -13987,7 +13987,7 @@ MonoBehaviour:
m_Calls:
- m_Target: {fileID: 4053823967713777864}
m_TargetAssemblyTypeName: ActionsSection, Assembly-CSharp
m_MethodName: UpdateAttributionPressed
m_MethodName: UpdateExternalAttributionPressed
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
Expand Down
24 changes: 20 additions & 4 deletions Assets/Scripts/AdaptyListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,16 +369,16 @@ public void ReportTransaction(Action<AdaptyError> completionHandler)
);
}

public void UpdateAttribution(Action<AdaptyError> completionHandler)
public void UpdateExternalAttribution(Action<AdaptyError> completionHandler)
{
this.LogMethodRequest("UpdateAttribution");
this.LogMethodRequest("UpdateExternalAttribution");

Adapty.UpdateAttribution(
Adapty.UpdateExternalAttribution(
"{\"test_key\": \"test_value\"}",
"custom",
(error) =>
{
this.LogMethodResult("UpdateAttribution", error);
this.LogMethodResult("UpdateExternalAttribution", error);
completionHandler.Invoke(error);
}
);
Expand Down Expand Up @@ -531,6 +531,22 @@ public void OnLoadLatestProfile(AdaptyProfile profile)
this.Router.SetProfile(profile);
}

public void OnReceivePromotedPurchase(AdaptyPromotedProduct product)
{
Debug.Log(
"#AdaptyListener# OnReceivePromotedPurchase called, product = "
+ product.VendorProductId
);

Adapty.MakePromotedPurchase(
product,
(result, error) =>
{
this.LogMethodResult("MakePromotedPurchase", error);
}
);
}

public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details)
{
Debug.Log(
Expand Down
4 changes: 2 additions & 2 deletions Assets/Scripts/Sections/ActionsSection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ public void SetIntegrationIdentifierPressed()
);
}

public void UpdateAttributionPressed()
public void UpdateExternalAttributionPressed()
{
this.Router.SetIsLoading(true);
this.Listener.UpdateAttribution(
this.Listener.UpdateExternalAttribution(
(error) =>
{
this.Router.SetIsLoading(false);
Expand Down
2 changes: 1 addition & 1 deletion Assets/StreamingAssets/adapty_fallback_android.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"data": {}, "meta": {"version": 10, "developer_ids": [], "response_created_at": 0}, "ui_builder": {}}
{"data": {}, "meta": {"version": 11, "developer_ids": [], "response_created_at": 0}, "ui_builder": {}}
10 changes: 9 additions & 1 deletion Assets/StreamingAssets/adapty_fallback_ios.json
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
{"data": {}, "meta": {"version": 10, "developer_ids": [], "response_created_at": 0}, "ui_builder": {}}
{
"data": {},
"meta": {
"version": 11,
"developer_ids": [],
"response_created_at": 0
},
"ui_builder": {}
}
122 changes: 122 additions & 0 deletions MIGRATION-v4.0-to-v4.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Migrate Adapty Unity SDK to v4.1

The good news first: this is a small migration, and the compiler does most of it for you. Plan for
three renames it will point out, one new interface method it will demand, and two things it cannot
know about: the fallback files both natives now reject, and the flag installation details now need.
Those two are the steps you can genuinely forget, so if you read just two sections, read
[Re-download your fallback files](#re-download-your-fallback-files) and
[Turn Adapty Attribution on if you read installation details](#turn-adapty-attribution-on-if-you-read-installation-details).

Under the hood, v4.1 moves the native dependencies to AdaptySDK-iOS 4.1.0 and AdaptySDK-Android
4.1.0, renames the external attribution API to match them, makes Adapty Attribution opt-in, and
adds App Store promoted purchases. The why behind each change, and what was fixed along the way, is
in [CHANGELOG.md](Packages/com.adapty.unity-sdk/CHANGELOG.md).

1. [Before you upgrade](#before-you-upgrade)
2. [Rename the attribution API](#rename-the-attribution-api)
3. [Implement the new listener method](#implement-the-new-listener-method)
4. [Re-download your fallback files](#re-download-your-fallback-files)
5. [Turn Adapty Attribution on if you read installation details](#turn-adapty-attribution-on-if-you-read-installation-details)
6. [Optional](#optional)

## Before you upgrade

If v4.0 built for you, v4.1 will too: the toolchain requirements did not move. Unity 2022.3 or
later, `com.unity.nuget.newtonsoft-json`, External Dependency Manager 1.2.188+ for iOS, Xcode 26 or
later, deployment target 15.0 or later — all exactly as in v4.0.

Coming from 3.x? Take [MIGRATION-v3.17-to-v4.0.md](MIGRATION-v3.17-to-v4.0.md) first — it covers
the parts that actually hurt (the paywall-to-flow rename, the Newtonsoft dependency, the install
order). This guide starts where it ends.

Both native dependencies move on their own. iOS is declared inside the package as a Swift package
and External Dependency Manager resolves it at build time; Android ships in the bundled
`.androidlib` that Unity adds to the Gradle build itself. There is nothing for you to update by
hand on either platform.

## Rename the attribution API

The native SDKs renamed their attribution APIs in 4.1, and the Unity SDK follows. There are no
deprecated aliases — deliberately: the old and new names would otherwise sit side by side in
autocomplete for a release cycle, and every existing call site is a two-word edit the compiler
finds for you anyway.

| v4.0 | v4.1 |
|---|---|
| `Adapty.UpdateAttribution(jsonString, source, handler)` | `Adapty.UpdateExternalAttribution(jsonString, provider, handler)` |
| `Adapty.UpdateAttribution(dictionary, source, handler)` | `Adapty.UpdateExternalAttribution(dictionary, provider, handler)` |
| `AdaptyProfile.AppliedAttributionSources` | `AdaptyProfile.AppliedExternalAttributionProviders` |

Only the names move. The provider is the same open `string` it always was (`"appsflyer"`,
`"adjust"`, `"branch"`, `"tenjin"`, `"apple_search_ads"`, `"custom"`), the profile member is still
an `IReadOnlyList<string>`, and the data you were sending keeps working unchanged.

## Implement the new listener method

`IAdaptyEventListener` gained a member, so every class implementing it stops compiling until you
add:

```csharp
public void OnReceivePromotedPurchase(AdaptyPromotedProduct product)
{
// The user tapped one of your in-app purchases on your App Store product page.
// Hand it back to Adapty to complete the purchase:
Adapty.MakePromotedPurchase(product, (result, error) => { /* ... */ });
}
```

Not sure whether you need this? Then you don't — promoted purchases are the ones you set up
manually in App Store Connect to appear on your App Store product page, and if you had, you would
know. An empty body is a perfectly honest implementation in that case, and the method is never
called on Android either way.

One thing worth knowing before you rely on it: the pinned AdaptySDK-iOS 4.1.0 does not yet hand
promoted purchases to wrappers — it completes them by itself, natively, without telling anyone. And
because the native dependency is pinned to exactly 4.1.0 — deliberately, so native behaviour never
changes underneath a wrapper that was not built for it — a future native release will not slip into
your build on its own. The handler starts receiving purchases once a future Unity SDK release moves
that pin to a native that reports them; implementing it now means your code is ready the day you
take that update.

## Re-download your fallback files

This is the step the compiler cannot catch, and the symptom shows up at runtime looking like a
broken integration: right after upgrading, `Adapty.SetFallback` fails. On iOS it reports
`DecodingFailed` (`adapty_code: 2006`, *"The fallback paywalls version is not correct. Download a
new one from the Adapty Dashboard."*); on Android, `WrongParam` (`adapty_code: 3001`, *"The
fallback file version is not correct. Download a new one from the Adapty Dashboard."*). Your
integration is fine — the file is stale.

Both natives reworked how fallback placements are read in 4.1 and now expect **fallback file format
11**; the format 10 files you exported for v4.0 no longer pass. The fix is exactly what the error
says: download fresh fallback files for both platforms from the Adapty Dashboard and replace the
ones in `Assets/StreamingAssets/`.

## Turn Adapty Attribution on if you read installation details

The other step nothing warns you about, and the one that looks least like a migration: in 4.1 the
natives collect installation details only when you ask them to. In v4.0 they always did.

So if your app implements `IAdaptyEventListener.OnInstallationDetailsSuccess` or calls
`Adapty.GetCurrentInstallationStatus`, the callback stops arriving and the status stops reporting
`AdaptyInstallationStatusType.Determined` — on both platforms — until you activate the service:

```csharp
var builder = new AdaptyConfiguration.Builder("PUBLIC_SDK_KEY")
.SetAdaptyAttributionEnabled(true);
```

Nothing else about it changed: the same details arrive in the same shape, on the same callback. If
your app never looked at installation details, there is nothing to do here — leaving the flag unset
is the same as before, minus the collection you were not using.

## Optional

- `AdaptyConfiguration.Builder.SetAdaptyAttributionEnabled(true)` turns on the
[Adapty Attribution](https://adapty.io/docs/attribution-integration) service. It is off by
default and not even sent unless you set it — the one thing that hangs off it is the installation
details covered above.
- That is the whole list. The v4.0 additions (`AdaptyUICreateFlowViewParameters.Locale`,
`AdaptyUIFlowView.Locale`) are now officially part of the cross-platform contract, and the rest
of the 4.1 wire-format changes — the nested offer identifier a purchase sends back, the
`ui_schema` a flow carries for the renderer — happen inside the SDK, where you never see them.
Loading
Loading