diff --git a/.claude/skills/contract-conformance/SKILL.md b/.claude/skills/contract-conformance/SKILL.md new file mode 100644 index 0000000..f99ec75 --- /dev/null +++ b/.claude/skills/contract-conformance/SKILL.md @@ -0,0 +1,158 @@ +--- +name: contract-conformance +description: Use when checking that the Unity SDK's C# models, enums and converters still match cross_platform.yaml — before a release, after bumping the native SDKs or the contract version, or when a payload behaves differently than the contract says it should +--- + +# Contract conformance + +`cross_platform.yaml` is the canonical description of every request, response and event that +crosses the bridge. The C# side restates it a second time, in attributes and hand-written +converters. Nothing in the build compares the two, so they drift apart silently. + +This skill is that comparison. `extract.py`, next to this file, does the mechanical half; the rest +is reading and judgement, and that is where the findings actually are. + +Work through the steps in order. Do not skip step 5 — the mechanical half agrees with itself and +still misses most of what matters. + +## What this cannot tell you + +Put these in the report rather than leaving them implied: + +- **The contract may be wrong, or ahead of the implementations.** It is maintained in AdaptySDK-iOS + and describes all platforms. A key can be declared there and implemented nowhere. +- **Error codes are not in the contract at all.** `AdaptyErrorCode` can only be checked against the + native Swift and Kotlin sources. Different job, do not attempt it here. +- **Value semantics** — units, ranges, what a value means. Shape and naming only. + + A `format` declared in the contract is the exception: it is part of the shape. If a key says + `format: "YYYY-MM-dd"`, whether the C# side actually produces that is in scope, and reading the + code may not settle it — hand-built strings are where this goes wrong. Serialize a value and look + at the bytes. Fixtures will not save you: a snapshot whose only date is `1815-12-10` passes + whether or not the writer pads single digits. + +## Step 1. Run the mechanical pass + +```bash +python3 -m venv /tmp/cc-venv && /tmp/cc-venv/bin/pip install --quiet pyyaml +``` + +```bash +/tmp/cc-venv/bin/python .claude/skills/contract-conformance/extract.py . --json /tmp/cc.json +``` + +It refuses to run if its own walk found implausibly little, so a clean exit means the input was +read. Read its first two lines and sanity-check the counts before trusting anything after them. + +## Step 2. Check the copy of the contract + +The repo's copy has to match the canonical one in AdaptySDK-iOS. If `.ios-sdk/` is checked out +(see the `ios-sdk-reference` skill) this is one command, not an act of faith: + +```bash +diff .ios-sdk/Sources.AdaptyPlugin/cross_platform.yaml cross_platform.yaml && echo IDENTICAL +``` + +If `.ios-sdk/` is missing, say in the report that this was not verified. Do not claim it matches. + +## Step 3. Triage what the script printed + +Every line it prints is a **candidate**, not a finding. Confirm each one by opening the file at the +line it names. Expect a good share to dissolve on contact — that is the script working as intended, +not failing. + +- **UNMAPPED** — a contract object with no C# type matched. Either map it in `MAPPING` or add it to + `NO_MODEL` with a reason, then rerun. Never leave one unexplained: a new contract object landing + unmapped is exactly the signal this whole exercise exists for. +- **C# TYPES WITH NO CONTRACT OBJECT** — usually request-shaping helpers and nested holders. For + each, satisfy yourself it is internal plumbing and not an invented wire shape. +- **contract key with no `[DataMember]`** — the script also tells you whether that string appears + anywhere in `Runtime/`. If it does, the key is probably supplied by a converter rather than an + attribute, and there is no defect; go read that line. If it appears nowhere, you likely have one. +- **required mismatch** — a key the contract requires in every `oneOf` branch that C# does not mark + `IsRequired`, or the reverse. Keys required in only some branches are annotated as conditional and + are usually fine. +- **platform** — the contract marks a key iOS/Android Only and C# does not gate it behind `#if`, or + the reverse. **Before calling this a defect, grep `CHANGELOG.md`, the surrounding comments and the + tests.** Some of these are deliberate: request-side parameter objects are intentionally ungated so + one call site compiles for every target. +- **STRING ENUMS** — a member in C# that the contract does not list is a defect. There is no + fallback: an unlisted string fails the read, and the only `Unknown` members left are the two the + contract spells out itself, on `AdaptyPaymentMode` and `AdaptySubscriptionPeriodUnit`. The reverse + is a defect too — a contract value with no member — and so is a member with no + `[EnumMember]`, which would be sent under its C# name. +- **WIRE NAMES** — every `method`/`id` constant in the contract, and whether that literal occurs in + `Runtime/`. An absent one is a request the SDK cannot make or an event it cannot receive. + +## Step 4. Cover what the script only lists + +The script prints converter `case` labels but does not compare them. Do it by hand: for every +`oneOf` in the contract that is not the generic `error`/`success` envelope, find its discriminator +values and match them against the converter that reads that type, or against the string literals in +the model when the discriminator is chosen by a static factory rather than a `switch`. + +Also read the converters in `Runtime/Serialization/` directly, one by one. Types built by a +converter have no `[DataMember]` at all, so nothing in step 3 says anything about them, and each +converter is a hand-written restatement of a contract object. For each key the converter reads, ask +whether the contract requires it and whether the converter enforces that — `AdaptyJsonRequire.*` enforces, +a plain `node["x"]?` does not. + +## Step 5. Ask the questions the script cannot + +This is where the findings are. + +- **The write path is not the read path.** For every value C# can produce, ask: can it be serialized + into a request, and does the contract allow it *there*? Requests often have their own contract + object, distinct from the response object of the same name — an enum listed in a response may have + fewer values allowed in the request that carries it back. +- **Optional keys the C# side cannot express.** A contract key that is optional and absent from C# + breaks nothing, and is still a capability the SDK does not offer. +- **Enforcement, not just presence.** A required key that is read leniently is a conformance gap even + though the property exists. + +## Rules that are not optional + +Two failure modes have already produced confident, wrong reports. Guard against both explicitly. + +1. **Re-read every line you cite.** Before using `file:line` as evidence — in your reasoning or in + the report — open that exact line and confirm it says what you think. A single misread line + reference has produced an entire well-argued finding about a divergence that did not exist, with + correct supporting quotes from native sources hung off a false premise. +2. **A rule inferred from a sample must be checked against the whole family.** If you conclude + "these enums all behave this way" or "this convention holds everywhere", enumerate every member + and check each. The exception is what you were looking for. A run that established a project-wide + rule from five of six enums missed the sixth, which was the only real finding in that area. +3. **When any line in a method draws your attention, read the whole method.** Whatever led you there + — a script candidate, a failing test, a diff — is not the only thing in it. A run that correctly + reported a lenient read of a required key missed a second lenient read of another required key + fifteen lines below it, in the same method, because only the first one had drawn attention. + +And: prefer a hedged finding to a confident one. If you cannot tell whether something is deliberate, +say so and say what you looked at — that is a useful report. A wrong classification sends someone +into three repositories after nothing. + +## Step 6. Report + +One entry per finding, each carrying: + +- **evidence on both sides** — `cross_platform.yaml` line, and `file.cs:line`, each re-read per + rule 1; +- **classification**: + - *our defect* — contract and native SDKs agree, the Unity side does not; + - *question for the contract owners* — the contract says something no implementation does, or the + implementations disagree with it. Check the native sources before choosing this over the first; + - *deliberate divergence* — differs on purpose, e.g. a Unity type with no wire equivalent, or + behaviour preserved from v3. Say what pins it: a comment, a test, a changelog entry. +- **whether it can fire today**, or only once a native SDK adds something. + +Close with coverage: how many contract objects were compared, how many keys, what was skipped and +why. A report that does not say what it did not look at is not finished. + +## Not your call + +Do not edit `cross_platform.yaml` — it is a copy of the canonical file, and changing it here only +hides the divergence. + +Do not add a member to a model because the contract has one, or remove one because it does not. +Either can be a public API change, and either may be the contract's mistake rather than ours. +Report, classify, stop. diff --git a/.claude/skills/contract-conformance/extract.py b/.claude/skills/contract-conformance/extract.py new file mode 100644 index 0000000..84ee01b --- /dev/null +++ b/.claude/skills/contract-conformance/extract.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 +"""Mechanical half of the contract-conformance check. + +Reads cross_platform.yaml and the C# sources and prints structural differences. It decides +nothing: every line it prints is a candidate to confirm by reading the source, and everything it +cannot map is handed back for a manual pass. + +Usage: python3 extract.py [--json out.json] +""" +import json +import pathlib +import re +import sys + +try: + import yaml +except ImportError: + sys.exit("pyyaml missing. Run: python3 -m venv /tmp/cc-venv && /tmp/cc-venv/bin/pip install pyyaml") + +ROOT = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 and not sys.argv[1].startswith("-") else ".") +SRC = ROOT / "Packages/com.adapty.unity-sdk/Runtime" + +# Contract objects whose C# name is not derivable from the contract name. Extend as the contract +# grows - anything absent from here and not auto-matched is reported as unmapped, loudly. +MAPPING = { + "CustomerIdentityParameters": "AdaptyCustomerIdentity", + "AdaptyPaywallProduct.Response": "AdaptyPaywallProduct", + "AdaptyPaywallProduct.Request": "AdaptyPaywallProductRequest", + "AdaptyPaywallProduct.Subscription": "AdaptySubscription", + "AdaptySubscriptionOffer.Phase": "AdaptySubscriptionPhase", + "AdaptyUI.FlowView": "AdaptyUIFlowView", + "AdaptyUI.OnboardingView": "AdaptyUIOnboardingView", + "AdaptyUI.OnboardingMeta": "AdaptyUIOnboardingMeta", + "AdaptyUI.DialogConfiguration": "AdaptyUIDialogConfiguration", + "AdaptyUI.UserAction": "AdaptyUIUserAction", + "AdaptyProfile.AccessLevel": "AdaptyProfile+AccessLevel", + "AdaptyProfile.NonSubscription": "AdaptyProfile+NonSubscription", + "AdaptyProfile.Subscription": "AdaptyProfile+Subscription", + "AdaptyFlowPaywall.ProductReference": "AdaptyFlowPaywall+ProductReference", + # $assets variants: one contract object, several C# types. Their members are unioned. + "Color": "AdaptyCustomAssetColor", + "ColorGradient": "AdaptyCustomAssetLinearGradient", + "Image": ["AdaptyCustomAssetLocalImageAsset", "AdaptyCustomAssetLocalImageFile", + "AdaptyCustomAssetLocalImageData"], + "Video": ["AdaptyCustomAssetLocalVideoAsset", "AdaptyCustomAssetLocalVideoFile"], +} + +# Contract objects that deliberately have no [DataMember] type behind them. The reason is the +# point: it is what a reviewer checks. Anything here is still worth a look by hand. +NO_MODEL = { + "AdaptySubscriptionOffer": "built by AdaptyConverterSubscriptionOffer - check the converter", + "AdaptySubscriptionOffer.Identifier": "flattened into AdaptySubscriptionOffer by the converter", + "AdaptyInstallationStatus": "polymorphic, built by AdaptyConverterInstallationStatus", + "AdaptyUI.OnboardingsStateParams": "polymorphic, built by AdaptyConverterOnboardingsStateUpdatedParams", + "AdaptyProfile.CustomAttributes": "free-form map, not a type", + "AdaptyUI.CustomTagsValues": "free-form map", + "AdaptyUI.CustomTimersValues": "free-form map", + "AdaptyUI.ProductPurchaseParameters": "free-form map", + "AdaptyUI.CustomAssets": "array of $assets variants", +} + +ENUM_MAPPING = { + "AdaptyLog.Level": "AdaptyLogLevel", + "AdaptyProfile.Gender": "AdaptyProfileGender", + "AdaptySubscriptionPeriod.Unit": "AdaptySubscriptionPeriodUnit", + "AdaptySubscriptionOffer.PaymentMode": "AdaptyPaymentMode", + "AdaptySubscriptionOffer.Identifier.Type": "AdaptySubscriptionOfferType", + "AdaptyWebPresentation": "AdaptyWebPresentation", + "AdaptyUI.DialogActionType": "AdaptyUIDialogActionType", +} + + +# --------------------------------------------------------------------------- contract +def flatten(node): + """Merge a schema object with its oneOf branches. + + oneOf hides properties inside the branches: reading only the top level makes every C# member + look invented. required_all is what every branch requires, required_any what any branch does. + """ + props, plat = {}, {} + top_req = set(node.get("required") or []) + branch_reqs = [] + + def take(d): + for k, v in (d.get("properties") or {}).items(): + props[k] = v + desc = v.get("description", "") if isinstance(v, dict) else "" + plat[k] = "ios" if "iOS Only" in desc else "android" if "Android Only" in desc else None + + take(node) + for b in node.get("oneOf") or []: + if isinstance(b, dict): + take(b) + branch_reqs.append(set(b.get("required") or [])) + + if branch_reqs: + return props, top_req | set.intersection(*branch_reqs), top_req | set.union(*branch_reqs), plat + return props, top_req, top_req, plat + + +def const_of(node, key): + for src in [node] + (node.get("oneOf") or []): + if isinstance(src, dict): + p = (src.get("properties") or {}).get(key) + if isinstance(p, dict) and "const" in p: + return p["const"] + return None + + +def load_contract(path): + doc = yaml.safe_load(path.read_text()) + types, enums, envelopes = {}, {}, {} + for section in ("$defs", "$assets"): + for name, node in (doc.get(section) or {}).items(): + if not isinstance(node, dict): + continue + if node.get("type") == "string" and node.get("enum"): + enums[name] = set(node["enum"]) + continue + props, req_all, req_any, plat = flatten(node) + if props: + types[name] = {"section": section, "props": props, "required_all": req_all, + "required_any": req_any, "platform": plat} + for section in ("$requests", "$events"): + for name, node in (doc.get(section) or {}).items(): + if not isinstance(node, dict): + continue + props, req_all, _, _ = flatten(node) + wire = const_of(node, "method") or const_of(node, "id") + envelopes[f"{section}/{name}"] = {"wire": wire, "props": sorted(props), + "required": sorted(req_all)} + return doc, types, enums, envelopes + + +# --------------------------------------------------------------------------- C# +CLASS = re.compile(r'^(\s*)(?:public|internal|private|protected)\s+(?:static\s+|sealed\s+|abstract\s+|partial\s+)*class\s+(\w+)') +DATAMEMBER = re.compile(r'\[DataMember\(([^\]]*)\)\]') +NAME = re.compile(r'Name\s*=\s*"([^"]+)"') +ISREQ = re.compile(r'IsRequired\s*=\s*true') +ENUM = re.compile(r'public enum (\w+)\s*(?::\s*\w+\s*)?\{(.*?)\n(\s*)\}', re.S) +ENUMMEMBER = re.compile(r'\[EnumMember\(Value\s*=\s*"([^"]+)"\)\]') + + +def load_csharp(src): + members, enums, cases, strings = {}, {}, {}, {} + for f in sorted(src.rglob("*.cs")): + text = f.read_text() + rel = str(f.relative_to(src.parent.parent.parent)) + + for m in ENUM.finditer(text): + vals = set(ENUMMEMBER.findall(m.group(2))) + enums[m.group(1)] = {"values": vals, "file": rel, "numeric": not vals} + + cls_stack, indent_stack, guard, pending = [], [], None, None + for lineno, line in enumerate(text.splitlines(), 1): + s = line.strip() + if s.startswith("#if"): + guard = "ios" if "UNITY_IOS" in s else "android" if "UNITY_ANDROID" in s else guard + continue + if s.startswith(("#endif", "#else")): + guard = None + continue + + c = CLASS.match(line) + if c: + ind = len(c.group(1)) + while indent_stack and indent_stack[-1] >= ind: + cls_stack.pop(); indent_stack.pop() + cls_stack.append(c.group(2)); indent_stack.append(ind) + + d = DATAMEMBER.search(line) + if d: + n = NAME.search(d.group(1)) + if n: + pending = (n.group(1), bool(ISREQ.search(d.group(1))), guard, lineno) + continue + if pending and s and not s.startswith(("//", "[", "///", "#")): + key, required, g, ln = pending + members.setdefault("+".join(cls_stack) or f.stem, {})[key] = { + "required": required, "platform": g, "file": rel, "line": ln} + pending = None + + for v in re.findall(r'case\s+"([^"]+)"', line): + cases.setdefault(f.stem, set()).add(v) + for v in re.findall(r'"([a-z][a-z0-9_.]{1,})"', line): + strings.setdefault(v, set()).add(f"{rel}:{lineno}") + return members, enums, cases, strings + + +def main(): + doc, types, cenums, envelopes = load_contract(ROOT / "cross_platform.yaml") + members, csenums, cases, strings = load_csharp(SRC) + + keys = sum(len(t["props"]) for t in types.values()) + print(f"contract: {len(types)} object types ({keys} keys), {len(cenums)} string enums, " + f"{len(envelopes)} request/event envelopes") + print(f"C#: {len(members)} types with [DataMember], {len(csenums)} enums") + if len(types) < 25 or keys < 150 or len(envelopes) < 80: + sys.exit("\nSTOP: the walk found implausibly little. Fix extraction before trusting anything below.") + + pairs, unmapped = {}, [] + for name in types: + t = MAPPING.get(name) or (name if name in members else None) + ts = [t] if isinstance(t, str) else (t or []) + ts = [x for x in ts if x in members] + if ts: + pairs[name] = ts + elif name not in NO_MODEL: + unmapped.append(name) + + print(f"\nmapped {len(pairs)}, no model by design {len(NO_MODEL)}, unmapped {len(unmapped)}") + + if unmapped: + print("\n" + "=" * 72) + print("UNMAPPED - map each one in MAPPING, or add it to NO_MODEL with a reason") + print("=" * 72) + for c in unmapped: + print(f" {c} keys: {sorted(types[c]['props'])[:6]}") + + orphans = sorted(set(members) - {t for ts in pairs.values() for t in ts}) + if orphans: + print("\n" + "=" * 72) + print("C# TYPES WITH [DataMember] AND NO CONTRACT OBJECT") + print("=" * 72) + for t in orphans: + print(f" {t}") + + print("\n" + "=" * 72) + print("STRUCTURAL DIFFERENCES - candidates, confirm each by reading the source") + print("=" * 72) + n = 0 + for cname, tnames in sorted(pairs.items()): + cd = types[cname] + md = {} + for tn in tnames: + for k, v in members[tn].items(): + md.setdefault(k, v) + tname = " | ".join(tnames) + miss = sorted(set(cd["props"]) - set(md)) + extra = sorted(set(md) - set(cd["props"])) + both = sorted(set(cd["props"]) & set(md)) + union = len(tnames) > 1 + reqd = [] if union else [k for k in both if (k in cd["required_all"]) != md[k]["required"]] + platd = [k for k in both if cd["platform"].get(k) != md[k]["platform"]] + if not (miss or extra or reqd or platd): + continue + n += 1 + print(f"\n{cname} -> {tname}") + for k in miss: + hint = f" [{cd['platform'][k]} only]" if cd["platform"].get(k) else "" + # Converters are the usual explanation for a key with no attribute, so show them first. + hits = sorted(strings.get(k, []), key=lambda s: ("Serialization/" not in s, s)) + print(f" contract key with no [DataMember]: {k}{hint}" + + (f" (string appears at {hits[:2]}, {len(hits)} site(s) total)" + if hits else " (string appears nowhere)")) + for k in extra: + print(f" [DataMember] with no contract key: {k} ({md[k]['file']}:{md[k]['line']})") + for k in reqd: + c_req = k in cd["required_all"] + print(f" required: {k} - contract={'required' if c_req else 'optional'}, " + f"C#={'IsRequired' if md[k]['required'] else 'optional'}" + f" ({md[k]['file']}:{md[k]['line']})" + + (" [conditional: required only in some oneOf branches]" + if not c_req and k in cd["required_any"] else "")) + for k in platd: + print(f" platform: {k} - contract={cd['platform'].get(k) or 'none'}, " + f"C# #if={md[k]['platform'] or 'none'} ({md[k]['file']}:{md[k]['line']})") + if not n: + print("\n none") + + print("\n" + "=" * 72) + print("STRING ENUMS") + print("=" * 72) + for name, vals in sorted(cenums.items()): + cs_name = ENUM_MAPPING.get(name, name.replace(".", "")) + cs = csenums.get(cs_name) + if cs is None: + print(f"\n {name}: {sorted(vals)}\n NO C# ENUM MATCHED - map it in ENUM_MAPPING") + continue + extra, miss = sorted(cs["values"] - vals), sorted(vals - cs["values"]) + flag = " <-- DIFFERS" if (extra or miss) else "" + print(f"\n {name} -> {cs_name}{flag}") + print(f" contract: {sorted(vals)}") + print(f" C#: {sorted(cs['values'])}") + if extra: + print(f" EXTRA IN C#: {extra} (fallback member? then check the WRITE path)") + if miss: + print(f" MISSING IN C#: {miss}") + + print("\n" + "=" * 72) + print("WIRE NAMES - every method/event id in the contract, and whether it appears in C#") + print("=" * 72) + absent = [(n, e["wire"]) for n, e in sorted(envelopes.items()) + if e["wire"] and e["wire"] not in strings] + print(f" {len(envelopes)} envelopes, {sum(1 for e in envelopes.values() if e['wire'])} carry a const name") + if absent: + for n, w in absent: + print(f" NOT FOUND IN C#: {w} ({n})") + else: + print(" every const method/event name appears somewhere in Runtime/") + + print("\n" + "=" * 72) + print("CONVERTER case LABELS - compare against the oneOf discriminators by hand") + print("=" * 72) + for f, vals in sorted(cases.items()): + print(f" {f}: {sorted(vals)}") + + if "--json" in sys.argv: + out = sys.argv[sys.argv.index("--json") + 1] + json.dump({"types": {k: {"props": sorted(v["props"]), "required_all": sorted(v["required_all"]), + "required_any": sorted(v["required_any"]), "platform": v["platform"]} + for k, v in types.items()}, + "members": members, "envelopes": envelopes, + "contract_enums": {k: sorted(v) for k, v in cenums.items()}, + "csharp_enums": {k: {"values": sorted(v["values"]), "numeric": v["numeric"]} + for k, v in csenums.items()}, + "pairs": pairs, "unmapped": unmapped}, open(out, "w"), indent=1) + print(f"\nwrote {out}") + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/ios-sdk-reference/SKILL.md b/.claude/skills/ios-sdk-reference/SKILL.md index 603a555..bcbae03 100644 --- a/.claude/skills/ios-sdk-reference/SKILL.md +++ b/.claude/skills/ios-sdk-reference/SKILL.md @@ -20,8 +20,9 @@ ls .ios-sdk/Sources 2>/dev/null ```bash git clone git@github.com:adaptyteam/AdaptySDK-iOS.git .ios-sdk ``` +If the machine's SSH config reaches GitHub through a host alias, use that host instead — check `git remote -v` in this repository for the form that works here. -**Step 3:** Determine which version/branch to use. Parse the current dependency version from `Assets/AdaptySDK/Editor/AdaptySDKDependencies.xml` (look for ``). Then **ask the user** which tag or branch to checkout, suggesting the dependency version as default. The user may want an unreleased branch instead. +**Step 3:** Determine which version/branch to use. Parse the current dependency version from `Packages/com.adapty.unity-sdk/Runtime/Editor/AdaptySDKDependencies.xml` (look for ``). Then **ask the user** which tag or branch to checkout, suggesting the dependency version as default. The user may want an unreleased branch instead. **Step 4:** Checkout the confirmed version: ```bash @@ -30,42 +31,54 @@ cd .ios-sdk && git fetch --all --tags && git checkout ## iOS SDK Directory Map +Verified against tag `4.0.2`. Re-check it after every checkout of a different tag — the layout moves between majors, and a map that lies is worse than no map. + ``` .ios-sdk/ ├── Sources/ # Core Adapty SDK │ ├── Adapty.swift # Main SDK class -│ ├── Adapty+*.swift # Public API extensions (GetPaywall, MakePurchase, etc.) +│ ├── Adapty+*.swift # Only three: Activate, Completion, Shared │ ├── Backend/ # HTTP API layer │ ├── Backend.HTTPSession/ # Network session │ ├── Configuration/ # SDK configuration -│ ├── Environment/ # Device/environment info +│ ├── Envoriment/ # Device/environment info - misspelled upstream, glob it that way +│ ├── Errors/ # AdaptyError and its codes │ ├── Events/ # Analytics events +│ ├── Log/ # Logging │ ├── Placements/ # Paywall placements │ ├── Profile/ # User profiles -│ ├── StoreKit/ # StoreKit integration +│ ├── Storage/ # Local caches +│ ├── StoreKit/ # StoreKit integration, and Adapty+MakePurchase.swift +│ ├── UserAcquisition/ # Install attribution +│ ├── WebPaywall/ # Web paywall URLs │ └── LifecycleManager.swift # App lifecycle │ ├── Sources.AdaptyPlugin/ # Cross-platform bridge (THIS IS THE KEY DIRECTORY) │ ├── AdaptyPlugin.swift # Main plugin entry: execute(method:withJson:) │ ├── cross_platform.yaml # API contract schema (JSON formats) -│ ├── Requests/ # One file per SDK method (37+ handlers) +│ ├── Requests/ # One file per SDK method (42 files, incl. AdaptyPluginRequest.swift) │ │ ├── Request.Activate.swift -│ │ ├── Request.GetPaywall.swift -│ │ ├── Request.MakePurchase.swift +│ │ ├── Request.AdaptyUICreateFlowView.swift +│ │ ├── Request.GetPaywallProducts.swift │ │ └── ... │ ├── Codable/ # JSON encoding/decoding for models │ └── Events/ # Event definitions pushed to Unity │ ├── Sources.AdaptyUI/ # Visual paywall rendering ├── Sources.UIBuilder/ # Paywall template builder -├── Sources.KidsMode/ # Kids mode support +├── Sources.Codable/ # Shared Codable helpers ├── Sources.Logger/ # Logging framework ├── Sources.DeveloperTools/ # Debug tools +├── Examples/ # Sample apps ├── Tests/ # Unit tests -├── Adapty.podspec # CocoaPods spec -└── Package.swift # Swift Package Manager manifest +├── scripts/ # Repo tooling +└── Package.swift # Swift Package Manager manifest, and where the traits live ``` +There is no `Sources.KidsMode/`: Kids Mode is a **trait** declared in `Package.swift`, which turns on the `KidsMode` compilation condition. The code it guards is `#if KidsMode` in `Sources/Envoriment/Environment.Device.idfa.swift`, `Sources/Adapty+Activate.swift` and `Sources/Profile/Entities/AdaptyProfileParameters.Builder.swift`. + +There is no `Adapty.podspec` either — the package ships through SwiftPM only, which is why the Unity side declares it with ``. + ## Common Lookup Patterns ### Find how a specific SDK method works on iOS @@ -73,8 +86,8 @@ cd .ios-sdk && git fetch --all --tags && git checkout # In Sources.AdaptyPlugin/Requests/ — one file per method Glob: .ios-sdk/Sources.AdaptyPlugin/Requests/Request.*.swift -# Example: how does GetPaywall work? -Read: .ios-sdk/Sources.AdaptyPlugin/Requests/Request.GetPaywall.swift +# Example: how does GetPaywallProducts work? +Read: .ios-sdk/Sources.AdaptyPlugin/Requests/Request.GetPaywallProducts.swift ``` ### Find JSON contract for a method @@ -99,10 +112,13 @@ Glob: .ios-sdk/Sources.AdaptyPlugin/Events/*.swift ### Find the core SDK implementation (not bridge) ``` -# Public API methods are in Adapty+MethodName.swift +# Only three files sit at the root: Adapty+Activate, Adapty+Completion, Adapty+Shared. Glob: .ios-sdk/Sources/Adapty+*.swift +# The rest live under the feature directory they belong to. # Example: full purchase flow -Read: .ios-sdk/Sources/Adapty+MakePurchase.swift +Read: .ios-sdk/Sources/StoreKit/Adapty+MakePurchase.swift +# When unsure which directory owns a method, search for it: +Grep: pattern="func makePurchase" path=".ios-sdk/Sources/" ``` ### Find model definitions in the core SDK @@ -114,20 +130,23 @@ Grep: pattern="struct Adapty" path=".ios-sdk/Sources/" When working on the Unity side, the mapping is: +Unity paths are relative to the repository root; everything in the package lives under `Packages/com.adapty.unity-sdk/`. + | Unity (C#) | iOS Bridge | iOS Core | |---|---|---| -| `Adapty.cs` methods | `Sources.AdaptyPlugin/Requests/Request.*.swift` | `Sources/Adapty+*.swift` | -| `Models/AdaptyFoo.cs` | `Sources.AdaptyPlugin/Codable/` | `Sources/` model files | -| `JSON/AdaptyFoo+JSON.cs` | `Sources.AdaptyPlugin/Codable/` | N/A | +| `Runtime/Adapty.cs` methods | `Sources.AdaptyPlugin/Requests/Request.*.swift` | `Sources//Adapty+*.swift` | +| `Runtime/Models/AdaptyFoo.cs` | `Sources.AdaptyPlugin/Codable/` | `Sources/` model files | +| `Runtime/Serialization/` (Newtonsoft layer) | `Sources.AdaptyPlugin/Codable/` | N/A | | `cross_platform.yaml` (Unity root) | `Sources.AdaptyPlugin/cross_platform.yaml` | N/A | -| `AdaptyEventListener.cs` | `Sources.AdaptyPlugin/Events/` | `Sources/Events/` | +| `Runtime/IAdaptyEventListener.cs` | `Sources.AdaptyPlugin/Events/` | `Sources/Events/` | +| `Runtime/AdaptyRequest.cs` (transport) | `Sources.AdaptyPlugin/AdaptyPlugin.swift` | N/A | ## Version Alignment The iOS dependency version is declared in: ``` -Assets/AdaptySDK/Editor/AdaptySDKDependencies.xml +Packages/com.adapty.unity-sdk/Runtime/Editor/AdaptySDKDependencies.xml ``` -Look for: `` +Look for: ``. It is a Swift Package Manager declaration read by External Dependency Manager, not a CocoaPods one. Always confirm with the user before checking out a tag — they may be working against an unreleased branch. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5ce3f6e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,136 @@ +# Adapted from Unity.gitattributes in github.com/gitattributes/gitattributes, MIT, copyright (c) +# 2015-2026 Alexander Karatarakis. The notice is in THIRD_PARTY_NOTICES.md, as that licence asks. +# +# One deliberate change against upstream: it routes every binary +# asset through Git LFS, and this repository does not use LFS - `git lfs ls-files` is empty, and +# turning the filter on would rewrite the .png, .unitypackage and .aar files already committed as +# ordinary blobs into pointers. They are marked `binary` here instead, which buys what is actually +# needed: no end-of-line conversion and no attempt to diff them. + +# Everything text is stored with LF, whatever the working tree uses. This is the rule that keeps a +# reformat done on one machine from rewriting every line of a file for the next reader. +* text=auto + +[attr]unity-yaml merge=unityyamlmerge eol=lf linguist-language=yaml +[attr]unity-json eol=lf linguist-language=json + +# Unity source files +*.cs text diff=csharp +*.cginc text +*.compute text linguist-language=hlsl +*.hlsl text linguist-language=hlsl +*.shader text + +# Unity JSON files +*.asmdef unity-json +*.asmref unity-json +*.inputactions unity-json +*.shadergraph unity-json +*.shadersubgraph unity-json + +# Unity YAML files +*.anim unity-yaml +*.asset unity-yaml +*.controller unity-yaml +*.mask unity-yaml +*.mat unity-yaml +*.meta unity-yaml +*.mixer unity-yaml +*.overrideController unity-yaml +*.playable unity-yaml +*.prefab unity-yaml +*.preset unity-yaml +*.renderTexture unity-yaml +*.scenetemplate unity-yaml +*.spriteatlas unity-yaml +*.terrainlayer unity-yaml +*.unity unity-yaml + +# "physic" for 3D but "physics" for 2D +*.physicMaterial unity-yaml +*.physicsMaterial2D unity-yaml + +# The one .asset Unity writes as JSON rather than YAML. `unity-json` does not clear the merge driver +# the rule above set, so it is unset by hand - feeding JSON to unityyamlmerge would not end well. +ProjectSettings/XRSettings.asset unity-json !merge + +# The native halves and the build files around them +*.gradle text +*.java text diff=java +*.kt text +*.swift text diff=swift +*.h text +*.m text diff=objc +*.mm text diff=objc +*.pbxproj text -diff merge=binary + +# ...except the only two this repository tracks, which are not generated projects but the fixtures +# KidsModeTraitTests round-trips between. Their diff is the review: the edit they pin is indentation +# exact, and "Binary files differ" would hide a change to what the test expects. +tests/shared/Fixtures/pbxproj/*.pbxproj text diff !merge +*.plist text +*.podspec text diff=ruby +Podfile text diff=ruby + +# Repository text that is not Unity's +*.json text +*.md text diff=markdown +*.properties text +*.pro text +*.txt text +*.xml text +*.yaml text +*.yml text + +# Scripts, where the ending is not cosmetic: cmd.exe wants CRLF, sh wants LF. The Gradle wrapper +# ships as both, and each half has to keep its own. +*.sh text eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf +gradlew text eol=lf + +# Binary: no end-of-line conversion, no diff. Not LFS - see the note at the top. +*.unitypackage binary +*.aar binary +*.jar binary +*.a binary +*.dll binary +*.dylib binary +*.pdb binary +*.so binary +*.bmp binary +*.exr binary +*.gif binary +*.hdr binary +*.jpeg binary +*.jpg binary +*.png binary +*.psd binary +*.tga binary +*.tif binary +*.tiff binary +*.webp binary +*.aif binary +*.aiff binary +*.mp3 binary +*.ogg binary +*.wav binary +*.mov binary +*.mp4 binary +*.otf binary +*.ttf binary +*.pdf binary +*.7z binary +*.gz binary +*.rar binary +*.tar binary +*.zip binary +*.apk binary +*.cubemap binary + +# Lightmap data is an .asset, but it is not YAML to merge. +LightingData.asset binary + +# Keep third-party trees and the lock file out of GitHub's language stats and diffs +Assets/Plugins/** linguist-generated +Packages/packages-lock.json linguist-generated diff --git a/.github/workflows/json-layer-tests.yml b/.github/workflows/json-layer-tests.yml new file mode 100644 index 0000000..d6a39d4 --- /dev/null +++ b/.github/workflows/json-layer-tests.yml @@ -0,0 +1,81 @@ +name: JSON layer tests + +# The SDK JSON layer against its approved snapshots. The suite links the SDK sources into a plain +# .NET project, so it needs neither the Unity Editor nor a licence, and runs once per define set: +# the layer branches on UNITY_IOS / UNITY_ANDROID and each has its own snapshots. The third leg is +# the Editor, which the projects define as UNITY_EDITOR when no platform is asked for - it is a +# configuration of its own, not the absence of one. The fourth is iOS with ADAPTY_KIDS_MODE, the +# one define that changes what goes on the wire rather than what compiles. + +on: + push: + branches: ["**"] + paths: + - "Packages/com.adapty.unity-sdk/Runtime/**" + # The suite links two Editor sources as well - AdaptyManifest and AdaptyIOSKidsModeTrait, the + # two that edit a build file by text - so a change to one of them has to run it. + - "Packages/com.adapty.unity-sdk/Editor/**" + - "tests/**" + - ".github/workflows/json-layer-tests.yml" + pull_request: + paths: + - "Packages/com.adapty.unity-sdk/Runtime/**" + # The suite links two Editor sources as well - AdaptyManifest and AdaptyIOSKidsModeTrait, the + # two that edit a build file by text - so a change to one of them has to run it. + - "Packages/com.adapty.unity-sdk/Editor/**" + - "tests/**" + - ".github/workflows/json-layer-tests.yml" + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + # The runners are UTC, where converting a date to UTC and relabelling it as UTC produce the + # same string - so DatesTheAppSuppliesAreWrittenAsUtc could not tell the two apart and would + # ignore itself. Any zone with a non-zero offset makes it discriminate again. + env: + TZ: Europe/Berlin + strategy: + fail-fast: false + matrix: + include: + - name: editor + defines: "" + - name: UNITY_IOS + defines: "UNITY_IOS" + - name: UNITY_ANDROID + defines: "UNITY_ANDROID" + # Kids Mode is the one shipped define that changes the wire format rather than which + # sources compile: on iOS it forces apple_idfa_collection_disabled, because the trait has + # compiled IDFA out of the binary. Without a leg of its own the three configuration + # snapshots it moves are approved for no one - the run simply failed all three. + # %3B escapes the ';' separating the two defines inside the MSBuild property value. + - name: UNITY_IOS-kids + defines: "UNITY_IOS%3BADAPTY_KIDS_MODE" + name: ${{ matrix.name }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Restore + run: dotnet restore tests/AdaptySDK.NextTests/AdaptySDK.NextTests.csproj + + - name: Test + run: | + if [ -z "${{ matrix.defines }}" ]; then + dotnet test tests/AdaptySDK.NextTests/AdaptySDK.NextTests.csproj --no-restore + else + dotnet test tests/AdaptySDK.NextTests/AdaptySDK.NextTests.csproj --no-restore \ + -p:AdaptyPlatform="${{ matrix.defines }}" + fi + + - name: Upload received snapshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: received-${{ matrix.name }} + path: tests/shared/Fixtures/approved/*.received.txt + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 1af7e7e..6942fe3 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,10 @@ ExportedObj/ *.aab *.app +# Staging area of build_unitypackage.sh. -p moves the artifact to the repository root, and +# release_unitypackage.sh moves it from there into Releases/, which is tracked. +/deploy/output/ + # ============================================================================= # IDEs # ============================================================================= @@ -125,5 +129,21 @@ settings.local.json # Generated SDK docs (not distributed via git) Assets/AdaptySDK/docs +# Documentation is never committed +docs/ + +# .NET build output of the golden tests +tests/**/bin/ +tests/**/obj/ +tests/**/*.received.txt + +# The *.csproj rule above is for the project files Unity generates at the repo root. The test +# suites are hand-written .NET projects and have to be tracked, or CI has nothing to restore. +!tests/**/*.csproj + # Platform build outputs /*/outputs/ + +# iOS player output of the demo build scripts +ios-sim-build/ +ios-stripped-build/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0d6ca0a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,167 @@ +# AGENTS.md + +This file provides guidance to coding agents working in this repository. It is the single copy — +`CLAUDE.md` points here rather than repeating any of it, so nothing has to be kept in sync. + +## Project Overview + +Adapty Unity SDK — a C# wrapper around native [Adapty iOS SDK](https://github.com/adaptyteam/AdaptySDK-iOS) (Swift/SPM) and [Adapty Android SDK](https://github.com/adaptyteam/AdaptySDK-Android) (Kotlin/Maven). Provides in-app purchase management, flow (paywall) rendering, onboarding flows, and subscription analytics for Unity apps. Current SDK version is defined in `Packages/com.adapty.unity-sdk/Runtime/Adapty.cs` (`Adapty.SDKVersion`). + +## Build & Development + +This is a **Unity project** (Unity 6000.x) — the player is built and tested through the Unity Editor. The JSON layer is the exception: `tests/` links the SDK sources into a plain .NET project, so it needs neither the Editor nor a licence. + +The package declares **Unity 2022.3 and newer** as `unity` in `package.json`, and that floor is what Editor-facing code may assume: `AdaptyDependencies` uses `Client.AddAndRemove` and `PackageInfo.FindForAssembly`, neither of which exists all the way back (`AddAndRemove` arrived after 2020.3). + +The install path is verified on the floor — `.unitypackage` import into a clean project, then `Adapty SDK > Install Dependencies`, then a compile, all on 2022.3.62f3. Everything else runs on Unity 6. Keep the changelog and `MIGRATION-v3.17-to-v4.0.md` wording matching that split; do not widen it to claim device or build coverage on 2022.3. One trap when re-verifying: recent 2022.3 builds are Extended LTS and refuse to launch without an Industry or Enterprise licence, so pick a build below that cutoff (62f3 works). + +**Run the JSON layer tests:** +```bash +dotnet test tests/AdaptySDK.NextTests/AdaptySDK.NextTests.csproj +``` +The layer branches on `UNITY_IOS` / `UNITY_ANDROID` and each platform has its own approved snapshots, so a change to it has to pass all four legs: add `-p:AdaptyPlatform=UNITY_IOS` or `-p:AdaptyPlatform=UNITY_ANDROID` for two of the others. Asking for no platform means `UNITY_EDITOR`, which the projects default to — the Editor is a configuration of its own and has to say so, since it is what selects the no-op bridge; an empty define set is a state Unity never produces. The fourth is `-p:AdaptyPlatform="UNITY_IOS%3BADAPTY_KIDS_MODE"` — `%3B` escapes the `;` inside the MSBuild property value. Kids Mode is the only shipped define that changes the wire format rather than which sources compile, and its whole visible effect here is the forced `apple_idfa_collection_disabled`: three configuration requests have a second approved form under `-kids`, and `RequestParityTests.Configured` is what picks it. Nothing else in the layer moves with the define — if a fourth snapshot ever needs a `-kids` form, that is a change in blast radius worth understanding before approving it. `ADAPTY_UPDATE_SNAPSHOTS=1` rewrites the approved files instead of failing. CI runs the same matrix in `.github/workflows/json-layer-tests.yml`. + +**Build .unitypackage for distribution:** +```bash +cd deploy && ./build_unitypackage.sh # export into deploy/output/ +cd deploy && ./build_unitypackage.sh -p # export, then move it to the repository root +``` +The export runs a second Editor in batch mode over a throwaway staging project: `Runtime/` copied to `Assets/AdaptySDK`, with `Editor/` merged into the `Editor` folder `Runtime` already contributes for `AdaptySDKDependencies.xml`. `.meta` files come across, so asset GUIDs survive and an upgrade lands on the files it replaces rather than beside them. The staging manifest is written from the package's own `dependencies`, so the export cannot be compiled against a Newtonsoft the package does not declare. `deploy/output/` is ignored by git; `Releases/` is tracked, and is where the release flow puts each artifact it publishes. It is not a complete history — `4.0.0-beta.1` was tagged without one — so it is not the source of the exact bytes of every past release. + +**Publish a release:** `deploy/release_unitypackage.sh` builds, moves the artifact into `Releases/`, commits, tags, pushes, and creates the GitHub release. Read its header before running it — it names three things it deliberately does not check, and each of them can publish something you did not mean to. `--dry-run` prints every command it would run and touches nothing. + +**Android wrapper (Java):** Built separately via Gradle in `adaptyandroidwrapper/`: +```bash +cd adaptyandroidwrapper && ./gradlew :unitywrapper:build +``` + +**Native dependency versions:** iOS is declared in `Packages/com.adapty.unity-sdk/Runtime/Editor/AdaptySDKDependencies.xml` (Swift Package Manager via External Dependency Manager 1.2.188+ — SPM support landed in 1.2.187, the Xcode project path for the Swift project type in 1.2.188; iOS deployment target 15.0+ is enforced by `Packages/com.adapty.unity-sdk/Editor/AdaptyIOSBuildValidator.cs`). **Xcode 26+ is the floor for the whole SDK on iOS**, not only for Kids Mode: AdaptySDK-iOS 4.0 declares `swift-tools-version: 6.2`, and SwiftPM refuses a package whose tools version exceeds the toolchain. Nothing in the Editor can check it, so it is a documented requirement only — re-check it on every native bump. Android is declared in `Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/AdaptySDKDependencies.androidlib/build.gradle`. Update both when bumping native SDK versions. + +## Architecture + +### Cross-Platform Bridge Pattern + +All SDK calls follow a single JSON-based bridge: + +1. **C# public API** (`Packages/com.adapty.unity-sdk/Runtime/Adapty.cs`, `Adapty.Overloads.cs`) — `static partial class Adapty` with methods like `GetFlow`, `MakePurchase`, etc. +2. Each method serializes parameters to JSON via `AdaptyRequest.Send()` or `AdaptyRequest.SendVoid()` (`Runtime/AdaptyRequest.cs`), which adds the `method` key and calls `_Adapty.Invoke(method, json, callback)`. Those two are the only way to the bridge: the raw transport is `private`, and both take the caller's name through `[CallerMemberName]`, so no call site writes a diagnostic string of its own. `AdaptyRequest.FailEncoding` is the third member of that surface and reaches no bridge at all — it reports a request that could not be encoded *before* it could be built, with the error the transport would have produced, and exists for the one overload that has to encode an argument of its own (`UpdateAttribution` taking a dictionary). It takes the caller's name the same way. Do not add a second caller without first asking whether the encoding could happen inside the guard instead. +3. **`_Adapty`** is compile-time aliased per platform: + - `AdaptySDK.iOS.AdaptyIOS` — P/Invoke `[DllImport("__Internal")]` to Swift plugin + - `AdaptySDK.Android.AdaptyAndroid` — `AndroidJavaClass` calling `com.adapty.unity.AdaptyAndroidWrapper` + - `AdaptySDK.Noop.AdaptyNoop` — no-op for Editor/unsupported platforms +4. Native side processes the JSON request and returns a JSON response string via callback. +5. Response is parsed back into C# models by Newtonsoft, through `AdaptySDK.Serialization.AdaptyJson`. + +### Newtonsoft Dependency + +`com.unity.nuget.newtonsoft-json` is a UPM dependency of the package, and it is not part of any stock Unity template — a `.unitypackage` carries assets only, so it cannot bring it along. Three rules follow, and all are load-bearing: + +- The **Runtime** assembly declares `ADAPTY_NEWTONSOFT` in `versionDefines` and requires it in `defineConstraints`. Without the package the assembly is skipped rather than failing to compile, which is what keeps a fresh import from spilling hundreds of `CS0246`. +- The **Editor** assembly must compile in every state, so it carries no constraint and no define of its own: it is what reports the problem (`AdaptyNewtonsoftValidator`) and installs the fix (`AdaptyDependencies`, the `Adapty SDK > Install Dependencies` menu item, which also installs External Dependency Manager and writes the OpenUPM registry it comes from into `Packages/manifest.json` — scoped registries have no public API). +- Presence is judged against the **package**, not the assembly, everywhere — `PackageInfo.FindForAssembly`. A `Newtonsoft.Json.dll` sitting in `Assets/` does not set the version define, so the SDK would silently not compile; the installer refuses to add a second copy on top of it and the validator names that state instead. EDM carries no define constraint, so any copy of it compiles — but the version decides whether iOS resolves, so a package-managed one below 1.2.188 is upgraded and one Package Manager does not describe is warned about rather than replaced. Only Package Manager can tell those apart: every 1.2.x build of `Google.VersionHandler` reports the same `1.2.0.0` assembly version. + +Editor code therefore cannot reference Runtime types, and the Editor asmdef's empty `references` is what enforces that. + +### Key Directory Layout + +- **`Packages/com.adapty.unity-sdk/`** — The SDK package distributed to users (UPM layout): + - `Runtime/Adapty.cs` — Main API (all public methods) + - `Runtime/AdaptyRequest.cs` — the transport: the two safe entry points, `FailEncoding` for a request that never got built, the private raw send, and the `_Adapty` alias that picks the platform bridge + - `Runtime/Adapty.Overloads.cs` — Convenience overloads with fewer parameters + - `Runtime/I*.cs` — one public interface per file, named after it: `IAdaptyEventListener`, `IAdaptyFlowsEventsListener`, `IAdaptyUISystemRequestsHandler`, `IAdaptyUIObserverModeResolver`. The deprecated `IAdaptyOnboardingsEventsListener` is under `Obsolete/` instead, by the rule below. + - `Runtime/Adapty.Events.cs` — the implementation behind them: listener registration, `OnMessage` and the `Dispatch` switch. Kept out of the interface files, so a contract and the code that calls it are not the same file. + - `Runtime/Models/` — C# data models (one file per type, e.g. `AdaptyFlow.cs`) + - `Runtime/Obsolete/` — everything `[Obsolete]`, in a tree mirroring `Runtime/` (`Models/`, `Serialization/Converters/`). The point is that removing the deprecated API is a directory deletion plus the references that then fail to compile, so **nothing outside this folder may carry the attribute** — `EveryObsoleteMemberLivesUnderObsolete` in `SourceConventionTests` is the check. Members of a live `partial class` live here as their own part: `Adapty.Obsolete.cs`, `AdaptyUI.Obsolete.cs`, `Adapty.Events.Obsolete.cs`. The two csproj globs are **not** recursive, so a new subfolder here needs its own `` line in both `tests/surface/package` and `tests/AdaptySDK.NextTests` or the SDK silently compiles without it. + - `Runtime/Serialization/` — the Newtonsoft JSON layer: `AdaptyJson` (the single entry point), `AdaptyContractResolver`, `AdaptyJsonRequire` (the required-key reads — a plain `node["x"]?` is what it exists instead of), `AdaptyResponse` (the reply side, and the one place `DecodingFailed` is raised), `AdaptyPaywallProductRequest`, and `Converters/`. Every converter lives in that folder, one per file, named `AdaptyConverter` after what it converts, and is registered in `AdaptyJson.Settings` — with one exception. `AdaptyConverterLooseJson` is deliberately **not** in the shared settings, so an ordinary `Dictionary` keeps Newtonsoft's own `JObject`/`JArray`/`Int64`. Three public payloads the contract types as a bare object must instead stay the CLR graph of doubles they were in 3.x, and each reaches the converter its own way: `AdaptyProfile.CustomAttributes` is a **member** and names it in a `[JsonConverter]` of its own — the one place a converter is declared outside `AdaptyJson`, and the reason that converter carries `[Preserve]`, since Newtonsoft then builds it by reflection — `AdaptyJson.DeserializeRemoteConfigDictionary` covers `AdaptyRemoteConfig.Dictionary`, which is a **string** parsed on demand, and `AdaptyJson.CreateSerializerFor` covers the dispatcher's `Required`/`Optional`, where `flow_view_did_receive_analytic_event` hands `params` straight to a listener. `CanConvert` is the single definition of "loose" that all three consult — do not restate the type list. Neither the fixtures nor the snapshots can see any of this: an integral `double` and a `long` print alike, and the profile fixture's only number is `12.5`. All three are pinned by type assertions instead. + - `Runtime/Plugins/iOS/` — `AdaptyIOS.cs` (P/Invoke bridge) + `Source/` (Swift/ObjC native plugin code) + - `Runtime/Plugins/Android/` — `AdaptyAndroid.cs` (JNI bridge) + `Local/` (local AAR maven repo) + `AdaptySDKDependencies.androidlib` (Android maven dependencies) + - `Runtime/Plugins/AdaptyNoop.cs` — Editor/no-op stub + - `Runtime/Editor/AdaptySDKDependencies.xml` — iOS Swift Package declaration for External Dependency Manager + - `Editor/` — Editor-only assembly (iOS build validation, Newtonsoft presence check, the `Adapty SDK > Install Dependencies` menu item, the Kids Mode trait edit) + +**Editor code a test can run.** Two things here edit a file the build depends on, by text, because neither has an API: `AdaptyManifest` writes the scoped registry into `Packages/manifest.json`, and `AdaptyIOSKidsModeTrait` writes the `KidsMode` trait onto the AdaptySDK-iOS package reference in the generated `project.pbxproj` — Unity's `PBXProject` models no such thing. Both are therefore **free of Unity types** and linked into `tests/AdaptySDK.NextTests` by their own ``; the `[PostProcessBuild]` step is a shim around the second, still under `#if UNITY_IOS && ADAPTY_KIDS_MODE`, and turns the one exception type the edit raises into a `BuildFailedException`. `AdaptyIOSBuildValidator` detects a build-profile define by asking whether the *post-processor* type exists, so that shim has to keep the define and its name. + +The trait fixtures are an excerpt of a real generated project, not a written-out sample: `tests/shared/Fixtures/pbxproj/` holds what External Dependency Manager wrote and what the build produced from it, and the test round-trips between them, which pins the indentation to the tab. Anything hand-made would only test the format this repository imagines. Regenerate both by building for iOS with `ADAPTY_KIDS_MODE` and taking the same excerpt — and note that the object ids differ on every build, so no test may name one. The edit locates the reference by `isa = XCRemoteSwiftPackageReference` and insists on exactly one — the brace nearest the URL alone would aim the insertion at whatever object enclosed some other occurrence, and a Kids Category build that reports Kids Mode while still linking IDFA is the one failure nothing downstream catches. + +Verifying a change here means a real **device** build — a simulator one cannot tell you anything, because `Environment.Device.idfa.swift` also guards the IDFA code with `targetEnvironment(simulator)`, so neither framework is linked with or without the trait. Two traps make that easy to get wrong: `StrippingBuild.IOS` never sets `PlayerSettings.iOS.sdkVersion`, so after an `IOSSimulator` run it stays `SimulatorSDK` and the "device" build is quietly a simulator one, and `ADAPTY_KIDS_MODE` has to reach the **Editor** assemblies before the build, since the post-processor is compiled with it - Player Settings is the reliable route, and an active build profile's defines work too once Unity has recompiled for them, which is the state `AdaptyIOSBuildValidator` checks for. What discriminates, measured: `-DKidsMode` in the compiler invocations, and `otool -L` on `UnityFramework` inside the built `.app` — 0 references with the trait, 2 without. +- **`adaptyandroidwrapper/`** — Standalone Android Gradle project: + - `unitywrapper/src/main/java/com/adapty/unity/` — `AdaptyAndroidWrapper.java` (entry point), callback handler, message handler +- **`tests/`** — .NET test projects for the JSON layer: `AdaptySDK.NextTests` (the suite), `shared/` (fixtures and snapshot helpers), `surface/` (the SDK compiled as a library to assert against), `aot-probe/`. Most of the suite asks the compiled assembly; `SourceConventionTests` and two checks in `ContractEnforcementTests` read the **sources** instead, for the things metadata cannot answer — `partial` does not survive compilation, an explicit enum value is indistinguishable from a counted one, and a file's directory is not a property of its types. +- **`Assets/Scripts/`** — Demo app scripts (not part of distributed SDK) +- **`cross_platform.yaml`** — Cross-platform API contract schema defining all request/response JSON formats and data types shared across iOS/Android/Unity + +### Event System + +Native SDKs push events (profile updates, flow view lifecycle, onboarding events) via the same JSON bridge. `Adapty.OnMessage(id, json)` in `Adapty.Events.cs` parses the payload and hands it to `Dispatch`, which switches on the event `id` and calls the registered listener interfaces. Nothing may escape `OnMessage`: the call arrives from native code with no handler behind it, so an exception takes the process down on IL2CPP rather than surfacing as a C# error. Every call into the app from the **live** API — a completion handler or a listener method — goes through `AdaptyCallbacks.InvokeSafe`, which holds the only implementation of the policy: safe means the app's exception is rethrown carrying the context of the call and the original as `InnerException`, not that it is swallowed. A **request** never names itself — `AdaptyRequest` supplies the wording from `[CallerMemberName]`, and that covers the deprecated onboarding requests too, which is what fixed the one call that used to hand the exception on raw. An **event** does name itself at the call site, because the listener method is not the enclosing one; the seven legacy onboarding events under `Obsolete/` keep their own hand-written wrappers, and that stays. `OnMessage`'s own guard is what actually contains it, and stays hand-written for that reason. Two event families are round-trips: flow permission requests are answered via `flow_view_did_answer_permission` (keyed by `event_id`), and Observer-mode purchases/restores report back via `observer_*_did_start/finish`. + +The seven `onboarding_*` ids are the exception to the one-switch rule: they leave `Dispatch` through `OnLegacyOnboardingMessage`, which is `[Obsolete]`. That is what keeps the deprecation of the legacy onboarding API from raising `CS0618` on every case of the main switch — folding them back multiplies the warnings by about thirty. `LegacyOnboardingDispatchTests` pins the routing of all seven. + +### Model Convention + +One file per model in `Runtime/Models/`, and no `partial` unless the type really is split — six are, each for a nested part or for its deprecated half under `Obsolete/`. Every concrete public class is `sealed`; the only open ones are the four abstract roots a converter picks between, and the approved public surface is what catches a new class that forgets. Serialization is declared with attributes, not written by hand: + +- `[DataContract]` on the type and `[DataMember(Name = "json_key")]` on each member, with `IsRequired = true` where the contract says the key is required. The JSON keys must match `cross_platform.yaml`, including which fields are required vs optional. +- `[Preserve]` on the type. Managed stripping otherwise removes it, and the failure shows only on a device, the first time a response carries the type. A nested type is covered by its declaring type's attribute; a member the serializer reaches through a method is not — a `[DataMember]` property, read through its getter, and an `[OnDeserialized]` callback — and needs its own. `StrippingGuardTests` asks the metadata for both. +- Conditional emission is a question for the model's own constructor, not for the serializer. A value the contract omits rather than sends empty is normalized to null where the object is built — `AdaptyConfiguration`'s builder does it for an identity carrying neither value, `AdaptyProductIdentifier`'s constructor for an empty base plan — and `NullValueHandling.Ignore` does the rest. There is no `ShouldSerializeX` convention: it existed in the resolver for three methods and was removed, so writing one now would silently do nothing for a field or a non-public member. +- A `oneOf` whose branches are one public object differing only by a discriminator and what that branch adds is **one flattened type** — a `Type`/`Status` enum plus the members, as `AdaptyPurchaseResult` and `AdaptyInstallationStatus` do. Where the branches are genuinely different shapes, the base class and its converter stay; that is the whole of `PolymorphicRoots` in `StrippingGuardTests` — `AdaptyCustomAsset`, `AdaptyOnboardingsAnalyticsEvent`, `AdaptyOnboardingsStateUpdatedParams`, `AdaptyOnboardingsInput` — and flattening one of those is not an improvement. A key the contract requires on one branch only is what no attribute can state: `AdaptyInstallationStatus` says it in a `[Preserve] [OnDeserialized]` method, which rejects a determined status without `details` and **drops** a `details` arriving on either other branch, matching what the subclass that had no such member did. Neither half is a rule about `oneOf` in general — `AdaptyPurchaseResult` normalizes nothing. Two things to know before writing a second such callback: Newtonsoft calls it through `MethodInfo.Invoke`, so whatever it throws arrives wrapped in a `TargetInvocationException` — harmless, since both boundaries a payload crosses catch `Exception` and print the inner one — and `StrippingGuardTests` will demand the `[Preserve]`. +- A collection a model hands back is a **read-only view over private concrete storage**: `[DataMember]` sits on a `private List`/`Dictionary`, and the public member is an `IReadOnlyList`/`IReadOnlyDictionary` wrapping it. The declared interface alone would not do — `ReadOnlyCollection` and `ReadOnlyDictionary` implement the mutable interfaces too, so the cast back compiles; what stops the write is that it yields the wrapper, which throws. The views are built in a `Freeze()` called from the constructor **and** from `[OnDeserialized]`, not beside the field: `ObjectCreationHandling.Replace` hands the deserializer a new collection, so a wrapper made in the initializer would wrap the discarded one and read empty forever. On the way in the rule is the mirror — take `IReadOnlyDictionary` and copy, so a caller still writing to its own dictionary cannot change what was handed over. +- **The wire is UTC; the public API is local.** A `DateTime` the SDK hands back is the same instant expressed on the machine's clock (`Kind == Local`), because these dates — a subscription's expiry, an access level's activation — are shown to end users, and `expiresAt > DateTime.Now` is what an app naturally writes. The write path is the mirror: anything the app supplies goes out as UTC, and a `Kind == Unspecified` value is read as local, since `new DateTime(2026, 7, 30, 22, 0, 0)` for a custom timer means 22:00 on the user's clock. `AdaptyConverterDateTime` owns both halves and cannot be replaced by `DateTimeZoneHandling.Utc`, which reads correctly but *relabels* an unspecified value on write instead of converting it — measured, and pinned by `DatesTheAppSuppliesAreWrittenAsUtc`. That test only discriminates off UTC, so CI sets `TZ`; on a UTC host it ignores itself rather than passing. +- Every public enum member states its number explicitly, and the numbers never move — they are public API even where the wire format is a string, and an inserted member would otherwise renumber everything below it. `EveryPublicEnumMemberStatesItsValue` reads the model sources for this, since metadata cannot tell an explicit value from a counted one; the approved public surface catches a number that moves. +- Enums follow one of two contracts, and the choice is part of the wire format. A **string** enum maps **every** member with `[EnumMember(Value = "...")]`, and a value outside that set fails the read: the SDK ships pinned to the native SDKs, so an unlisted string is a broken payload rather than one from the future. No string enum has an `Unknown` fallback except two — `AdaptyPaymentMode` and `AdaptySubscriptionPeriodUnit` — and those two have the member because the contract lists `"unknown"` among *their* values. `AdaptyErrorCode.Unknown = 0` is not an exception to that: it is a **numeric** enum, and 0 is a code the native side declares and can send like any other — `case unknown = 0` in AdaptySDK-iOS, for a failure it could not classify. Where the contract does want an open set it says so, and the model holds a `string`: a flow permission, an onboarding event name. A **numeric** enum — `AdaptyErrorCode`, `AppTrackingTransparencyStatus` — carries the native number and declares no `[EnumMember]` at all: `AdaptyConverterStringEnum.CanConvert` skips it and Newtonsoft's default numeric handling applies. Adding `[EnumMember]` to a numeric enum silently switches it to strings. `AdaptyConverterStringEnum` writes through stock `StringEnumConverter` but **reads with its own ordinal map**, because stock reading is lenient in three ways the contract does not allow: it accepts the C# member name as well as the `[EnumMember]` one, ignores case, and trims the value — so `"UserCancelled"`, `"USER_CANCELLED"` and `" user_cancelled "` would all pass for `"user_cancelled"`. Neither half of the converter can report a member with no `[EnumMember]` (stock writing falls back to the C# name) or two members sharing one, so `EveryMemberOfAContractNamedEnumHasItsName` asserts both over the metadata. + +`tests/AdaptySDK.NextTests` enforces all of this: a model added without `[Preserve]` fails `StrippingGuardTests`, and one whose output changes fails its approved snapshot. + +### Overloads + +The completion handler is the last parameter of every public method, is never optional, and never has a default — the SDK does not offer a fire-and-forget call, so no method taking a completion handler has an optional parameter at all. The two defaults that do exist are on a constructor, `AdaptyPurchaseParameters`, where both are trailing and stable, which is the one shape defaults are for. That is what forces the convenience forms to be **overloads** rather than trailing defaults: in every group the optional-looking argument (`fetchPolicy`, `purchaseParameters`, `variationId`, a presentation style) sits *before* the callback, and C# has no required parameter after an optional one. Collapsing them would mean making the callback optional or moving it, and neither is on the table. + +Each short form is a one-line forward to the canonical method — audited, none carries logic of its own. Five groups instead overload by the *type* of the first argument (`Activate`, `OpenWebPaywall`, `CreateWebPaywallUrl`, `ShowDialog`, `UpdateAttribution`); passing a literal `null` there is ambiguous and fails as `CS0121` — measured on two of them — which is loud and acceptable. + +### Static state and Play Mode + +With Domain Reload disabled — the default for fast iteration — Unity keeps static fields between Play Mode runs. Anything the SDK holds **on the developer's behalf** has to be cleared at `[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]`, which runs before the first scene of every run: today the four listeners and the no-op bridge's test hook. The legacy onboarding listener is cleared from its own part under `Obsolete/`, marked `[Obsolete]` itself, so a live method does not reference a deprecated field and raise `CS0618` where the caller has nothing to act on. + +Infrastructure is **not** reset — the contract resolver, the settings, the converters' type caches. It is derived from the assembly, identical every run, and rebuilding it would only cost startup time. Nor is the native bridge's registration flag: clearing it separately from the native side would register a callback twice. + +Not every `[RuntimeInitializeOnLoadMethod]` here is a reset. `Adapty.InitializeTransport` registers the platform callback bridge, and it exists because registration used to happen in the four listener setters and nowhere else — so an app that never subscribed to events got no completion back from any request, on either platform, and iOS leaked the handle for each one. The stage is `BeforeSceneLoad`: it covers the whole MonoBehaviour lifecycle, which is what the SDK guarantees, and the environment is fully up there, which matters because this one crosses into JNI — the earlier stages would buy only the case of an app reaching the SDK from a hook of its own, and that case is deliberately outside the guarantee. That is also why there is no second registration anywhere else: not inside the platform `Invoke`, and no longer in the listener setters, which is where it used to be and where five call sites made it impossible to tell which one the SDK depended on. `InitializeOnce` has exactly one caller now, and the idempotence guard inside it stays regardless — it belongs to the platform bridge, not to the count of callers. `EveryResetIsRegisteredWithUnity` matches `Reset*` and does not see this method — `TheTransportIsRegisteredBeforeTheFirstScene` is what guards it, and what it pins is the stage, since `AfterSceneLoad` runs once a scene has already had the chance to call the SDK. + +`EveryResetIsRegisteredWithUnity` is the guard — it checks the load type too, not just the attribute: `AfterSceneLoad` runs once a scene has already had the chance to register a listener, so a reset there would clear the new run's own. The Editor assembly is out of reach of all this, and keeps its own `[InitializeOnEnterPlayMode]` in `AdaptyDependencies` for the one subscription that does not clean itself up — a Package Manager request left in flight. What no desktop test can show is that Unity calls it at all, and that is the failure to fear — the `AdaptyDependencies` callback above once had the wrong signature and was dead code that compiled. `Assets/Tests/PlayMode/` covers it and runs in batch: `Assets/Editor/AdaptyPlayModeSeed.cs` registers a listener from `[InitializeOnEnterPlayMode]`, which is where a previous run's listener would still be sitting, and the test asserts by reflection that all four fields came out null. Run it with `-batchmode -runTests -testPlatform PlayMode`. It is only valid while `EditorSettings` keeps Enter Play Mode Options on with Domain Reload disabled — with the reload on, the statics are cleared by the domain being rebuilt and the test passes with no reset in the SDK at all, so a second case asserts the setting and fails first. + +**The seed leaves the entry's id, not a flag, and `AdaptyPlayModeEntryStamp` is why** — a marker saying only "a seed ran" is one an earlier entry can have left, which would make the run green having measured nothing. Both files carry the reasoning; do not reduce the id to a flag without reading it. + +### 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. + +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. + +The public surface is byte-identical on all three platforms; the conditionals change what is read and written, never what is declared. Keep it that way — `diff`ing the three approved surface files is the check. + +**Decided for 4.0, not an oversight:** on an Android device `UpdateAppStoreCollectingRefundDataConsent`, `UpdateAppStoreRefundPreference` and `PresentCodeRedemptionSheet` report `null` — indistinguishable from success. That is a silent no-op rather than an unsupported-platform error, and the call never reaches a bridge that would say so. It stays that way here: changing it is a behaviour change for callers and belongs in a release that expects one. Do not "fix" it in passing. The Editor half was never the same question and is fixed — it used to take the same branch, which contradicted the changelog's own promise that a call in the Editor reports a readable error. + +### Deprecation + +Deprecating one entry point is not enough — mark everything the deprecated API hands back or takes, or the warning only reaches the caller at the registration call and never at the type they wrote. The attribute is written `[System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")]`, with the same sentence everywhere. Marking a member is also what decides where it lives: it moves to `Runtime/Obsolete/`, and the two travel together. + +**The legacy onboarding API has to keep working, and nothing more.** It is still in the shipped public surface and apps still call it, so a bug in it gets fixed. Everything else is explicitly not owed to it: **its failing to meet a convention this document sets is not a defect**, needs no note and no follow-up item. It kept its polymorphic converters through the converter audit and its mutable `IList` through the read-only collections work, and that is the expected outcome, not debt. The line is what a change touches: repository-wide formatting applies to it like anywhere else — headers, `using` placement, `is not` type patterns — while anything reaching its API, a renamed parameter included, does not. Breaking changes to it are undesirable — the one structural change it did get, the move to `Runtime/Obsolete/`, touched no name, signature or type. When a sweep states a rule in the changelog, say plainly that the deprecated API is the exception rather than implying it was covered: an inaccurate claim about it is a defect, touching it to make the claim true is not. + +Marking a public type deprecates it for the SDK's own code too, so expect `CS0618` inside the package, and expect it to reach the console of everyone who installs the SDK: the Editor reports it. `CS0649` is the opposite case and does not — Unity passes `/nowarn:0649` and `/nowarn:0169` to every assembly it compiles, which is why the package's reflection-assigned fields are silent there while `tests/surface` has to suppress them itself. Never silence `CS0618` — `#pragma warning disable` is not used in this repository. Mark the internal parts that serve the deprecated API instead (private fields, helpers, converters): a reference from obsolete code to obsolete code raises nothing, which pushes the warnings back to the boundary where live code really does touch the deprecated API. Those remaining warnings are the point, not a problem to solve. + +The public surface snapshots record signatures without attributes, so nothing fails if `[Obsolete]` is dropped from a member. + +## Version Bumping + +When releasing a new version, update: +1. `Adapty.SDKVersion` in `Packages/com.adapty.unity-sdk/Runtime/Adapty.cs` +2. `version` in `Packages/com.adapty.unity-sdk/package.json` +3. Native dependency versions: iOS in `Runtime/Editor/AdaptySDKDependencies.xml`, Android in `Runtime/Plugins/Android/AdaptySDKDependencies.androidlib/build.gradle` and `adaptyandroidwrapper/unitywrapper/build.gradle` (then rebuild the AAR into `Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper//`, and **delete the previous one** — no `build.gradle` in the package references the artifact, Unity picks up any `.aar` under `Plugins/Android` as a plugin, so two versions side by side both reach the player) +4. `cross_platform.yaml` schema `$id` version — must match the canonical contract in AdaptySDK-iOS (`Sources.AdaptyPlugin/cross_platform.yaml`); diff the two files, not just the version +5. `CHANGELOG.md` and the `_upm.changelog` string in `package.json` — keep both in sync, the latter is what Package Manager shows after an update. In step by the **set of entries**, not by wording: the Package Manager copy is deliberately terser, and comparing the two byte for byte only produces noise. What it must not do is drop one — a missing entry is invisible until someone reads the two side by side. The heading also lives in `CHANGELOG.md` alone: `_upm.changelog` holds the body of the section, with neither the version nor the date, so the release date is set in one place on the day of the cut +6. Managed dependency versions, when they move: `dependencies` and `peerDependencies` in `package.json` **and** the constants in `Editor/AdaptyDependencies.cs`, which is what installs them for `.unitypackage` users +7. The two places a version is written into a URL: the `#` on the Package Manager install URL in `README.md`, and the `blob//` the new `CHANGELOG.md` section and its `_upm.changelog` copy use to reach `MIGRATION-v3.17-to-v4.0.md`. Both are pinned deliberately. `main` carries the previous major until a release merges into it, so an unpinned install URL silently resolves to that major, and a `blob/main` link to a file added by this release is a 404 for as long as the gap lasts. A changelog section keeps the tag it was written for — old sections are not re-pinned + +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. + +`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. diff --git a/Assets/CharlesProxy/Editor/CharlesAndroidManifestUpdater.cs b/Assets/CharlesProxy/Editor/CharlesAndroidManifestUpdater.cs index f86132e..8319551 100644 --- a/Assets/CharlesProxy/Editor/CharlesAndroidManifestUpdater.cs +++ b/Assets/CharlesProxy/Editor/CharlesAndroidManifestUpdater.cs @@ -1,95 +1,95 @@ -using System.IO; -using System.Text; -using System.Xml; -using UnityEditor.Android; -using UnityEngine; - -public class CharlesAndroidManifestUpdater : IPostGenerateGradleAndroidProject -{ - public void OnPostGenerateGradleAndroidProject(string basePath) - { - // If needed, add condition checks on whether you need to run the modification routine. - // For example, specific configuration/app options enabled - var androidManifest = new AndroidManifest(GetManifestPath(basePath)); - - // Add your XML manipulation routines - androidManifest.SetNetworkSecurityConfig(); - - //Save the new manifest - androidManifest.Save(); - } - - public int callbackOrder { get { return 1; } } - - private string _manifestFilePath; - - private string GetManifestPath(string basePath) - { - if (string.IsNullOrEmpty(_manifestFilePath)) - { - var pathBuilder = new StringBuilder(basePath); - pathBuilder.Append(Path.DirectorySeparatorChar).Append("src"); - pathBuilder.Append(Path.DirectorySeparatorChar).Append("main"); - pathBuilder.Append(Path.DirectorySeparatorChar).Append("AndroidManifest.xml"); - _manifestFilePath = pathBuilder.ToString(); - } - return _manifestFilePath; - } -} - - -internal class AndroidXmlDocument : XmlDocument -{ - private string m_Path; - private XmlNamespaceManager m_NsMgr; - protected readonly string m_AndroidXmlNamespace = "http://schemas.android.com/apk/res/android"; - - protected AndroidXmlDocument(string path) - { - m_Path = path; - using (var reader = new XmlTextReader(m_Path)) - { - reader.Read(); - Load(reader); - } - m_NsMgr = new XmlNamespaceManager(NameTable); - m_NsMgr.AddNamespace("android", m_AndroidXmlNamespace); - } - - public string Save() - { - return SaveAs(m_Path); - } - - public string SaveAs(string path) - { - using (var writer = new XmlTextWriter(path, new UTF8Encoding(false))) - { - writer.Formatting = Formatting.Indented; - Save(writer); - } - return path; - } -} - - -internal class AndroidManifest : AndroidXmlDocument -{ - private readonly XmlElement m_ApplicationElement; - - public AndroidManifest(string path) : base(path) - { - m_ApplicationElement = SelectSingleNode("/manifest/application") as XmlElement; - } - - internal void SetNetworkSecurityConfig() { - m_ApplicationElement.Attributes.Append(CreateAndroidAttribute("networkSecurityConfig", "@xml/network_security_config")); - } - - private XmlAttribute CreateAndroidAttribute(string key, string value) - { - XmlAttribute attr = CreateAttribute("android", key, m_AndroidXmlNamespace); - attr.Value = value; - return attr; - } -} +using System.IO; +using System.Text; +using System.Xml; +using UnityEditor.Android; +using UnityEngine; + +public class CharlesAndroidManifestUpdater : IPostGenerateGradleAndroidProject +{ + public void OnPostGenerateGradleAndroidProject(string basePath) + { + // If needed, add condition checks on whether you need to run the modification routine. + // For example, specific configuration/app options enabled + var androidManifest = new AndroidManifest(GetManifestPath(basePath)); + + // Add your XML manipulation routines + androidManifest.SetNetworkSecurityConfig(); + + //Save the new manifest + androidManifest.Save(); + } + + public int callbackOrder { get { return 1; } } + + private string _manifestFilePath; + + private string GetManifestPath(string basePath) + { + if (string.IsNullOrEmpty(_manifestFilePath)) + { + var pathBuilder = new StringBuilder(basePath); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("src"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("main"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("AndroidManifest.xml"); + _manifestFilePath = pathBuilder.ToString(); + } + return _manifestFilePath; + } +} + + +internal class AndroidXmlDocument : XmlDocument +{ + private string m_Path; + private XmlNamespaceManager m_NsMgr; + protected readonly string m_AndroidXmlNamespace = "http://schemas.android.com/apk/res/android"; + + protected AndroidXmlDocument(string path) + { + m_Path = path; + using (var reader = new XmlTextReader(m_Path)) + { + reader.Read(); + Load(reader); + } + m_NsMgr = new XmlNamespaceManager(NameTable); + m_NsMgr.AddNamespace("android", m_AndroidXmlNamespace); + } + + public string Save() + { + return SaveAs(m_Path); + } + + public string SaveAs(string path) + { + using (var writer = new XmlTextWriter(path, new UTF8Encoding(false))) + { + writer.Formatting = Formatting.Indented; + Save(writer); + } + return path; + } +} + + +internal class AndroidManifest : AndroidXmlDocument +{ + private readonly XmlElement m_ApplicationElement; + + public AndroidManifest(string path) : base(path) + { + m_ApplicationElement = SelectSingleNode("/manifest/application") as XmlElement; + } + + internal void SetNetworkSecurityConfig() { + m_ApplicationElement.Attributes.Append(CreateAndroidAttribute("networkSecurityConfig", "@xml/network_security_config")); + } + + private XmlAttribute CreateAndroidAttribute(string key, string value) + { + XmlAttribute attr = CreateAttribute("android", key, m_AndroidXmlNamespace); + attr.Value = value; + return attr; + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON.meta b/Assets/Editor.meta similarity index 77% rename from Packages/com.adapty.unity-sdk/Runtime/JSON.meta rename to Assets/Editor.meta index 0289247..433b8ad 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON.meta +++ b/Assets/Editor.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 5c66bfd1a876944739d8b2e0c58c9f74 +guid: 52e2ae420131145399146f25933e21ca folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Editor/AdaptyPlayModeEntryStamp.cs b/Assets/Editor/AdaptyPlayModeEntryStamp.cs new file mode 100644 index 0000000..39f9da7 --- /dev/null +++ b/Assets/Editor/AdaptyPlayModeEntryStamp.cs @@ -0,0 +1,48 @@ +using System; +using UnityEditor; +using UnityEngine; + +/// +/// Gives every entry into Play Mode an identity of its own, and wipes what the entry before it left +/// behind — both before gets to write anything. +/// +/// +/// This is deliberately not part of the seed. The failure it defends against is the seed not +/// running: an entry that seeded and then never reached Play Mode leaves its marker on disk, and +/// outlive not just the entry but the Editor session. A marker is +/// therefore accepted only while it names the entry now under way, and the seed cannot name it +/// without running — while whether it ran is exactly what the Play Mode test is asking. +/// +[InitializeOnLoad] +public static class AdaptyPlayModeEntryStamp +{ + /// + /// The entry now under way. Rotated on the way into every Play Mode entry, never consumed. + /// + public const string EntryKey = "adapty.playmode.entry"; + + /// + /// What the seed leaves for the test: <entry>:<UTC ticks>:<1 if Domain Reload + /// is off>. Owned here rather than by the seed, so removing the seed cannot take the + /// cleanup with it. + /// + public const string SeedKey = "adapty.playmode.seed"; + + static AdaptyPlayModeEntryStamp() + { + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + } + + private static void OnPlayModeStateChanged(PlayModeStateChange change) + { + if (change != PlayModeStateChange.ExitingEditMode) + { + return; + } + + PlayerPrefs.SetString(EntryKey, Guid.NewGuid().ToString("N")); + PlayerPrefs.DeleteKey(SeedKey); + PlayerPrefs.Save(); + } +} diff --git a/Assets/Editor/AdaptyPlayModeEntryStamp.cs.meta b/Assets/Editor/AdaptyPlayModeEntryStamp.cs.meta new file mode 100644 index 0000000..17835c9 --- /dev/null +++ b/Assets/Editor/AdaptyPlayModeEntryStamp.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a874bc76779264e04ba355c2ac756b0f \ No newline at end of file diff --git a/Assets/Editor/AdaptyPlayModeSeed.cs b/Assets/Editor/AdaptyPlayModeSeed.cs new file mode 100644 index 0000000..f5ac2d9 --- /dev/null +++ b/Assets/Editor/AdaptyPlayModeSeed.cs @@ -0,0 +1,59 @@ +using System; +using System.Globalization; +using AdaptySDK; +using UnityEditor; +using UnityEngine; + +/// +/// Leaves a listener registered just before Play Mode starts, so a Play Mode test can tell whether +/// the SDK's own reset ran. +/// +/// +/// With Domain Reload disabled a listener registered by the previous run survives into the next +/// one, and this seed is that leftover: InitializeOnEnterPlayMode runs before +/// RuntimeInitializeLoadType.SubsystemRegistration, so the SDK's reset sees it exactly as it +/// would see a real one. The point is not that the reset clears the field — a desktop test already +/// calls it directly — but that Unity calls it at all. +/// +public static class AdaptyPlayModeSeed +{ + private sealed class Sink : IAdaptyEventListener + { + public void OnLoadLatestProfile(AdaptyProfile profile) { } + + public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { } + + public void OnInstallationDetailsFail(AdaptyError error) { } + } + + /// + /// Registers the listener and names the entry it was registered for. + /// + /// + /// The marker is that entry's own id rather than a flag or a timestamp of this method's + /// choosing: rotates the id on the way into every entry, + /// so a marker this method did not write for the entry now under way cannot match. The + /// timestamp beside it is a second bound, for the case where the rotation stops happening. + /// + [InitializeOnEnterPlayMode] + private static void Seed(EnterPlayModeOptions options) + { + Adapty.SetEventListener(new Sink()); + + var noDomainReload = + EditorSettings.enterPlayModeOptionsEnabled + && EditorSettings.enterPlayModeOptions.HasFlag(EnterPlayModeOptions.DisableDomainReload); + + PlayerPrefs.SetString( + AdaptyPlayModeEntryStamp.SeedKey, + string.Format( + CultureInfo.InvariantCulture, + "{0}:{1}:{2}", + PlayerPrefs.GetString(AdaptyPlayModeEntryStamp.EntryKey, string.Empty), + DateTime.UtcNow.Ticks, + noDomainReload ? 1 : 0 + ) + ); + PlayerPrefs.Save(); + } +} diff --git a/Assets/Editor/AdaptyPlayModeSeed.cs.meta b/Assets/Editor/AdaptyPlayModeSeed.cs.meta new file mode 100644 index 0000000..d344f0d --- /dev/null +++ b/Assets/Editor/AdaptyPlayModeSeed.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6030e86edab0e40bbb00b5d824a57cb2 \ No newline at end of file diff --git a/Assets/Editor/StrippingBuild.cs b/Assets/Editor/StrippingBuild.cs new file mode 100644 index 0000000..ef3c282 --- /dev/null +++ b/Assets/Editor/StrippingBuild.cs @@ -0,0 +1,110 @@ +using System; +using System.Linq; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using UnityEngine; + +/// +/// Builds the demo with managed stripping set to High. +/// +/// +/// High is the setting the serialization layer has to survive: it is what removes the constructors +/// and members a reflection-based serializer reaches for, and the reason the package annotates its +/// models with [Preserve]. The project's own setting is Low, so it is raised here rather than in +/// ProjectSettings - the migration has to prove the strict case, not change what the demo ships +/// with. +/// +public static class StrippingBuild +{ + public static void IOS() => Build(BuildTarget.iOS, NamedBuildTarget.iOS, "ios-stripped-build"); + + /// + /// The simulator player, still at stripping High: the scenario run has to exercise the same + /// configuration the device build does. + /// + public static void IOSSimulator() + { + PlayerSettings.iOS.sdkVersion = iOSSdkVersion.SimulatorSDK; + Build(BuildTarget.iOS, NamedBuildTarget.iOS, "ios-sim-build"); + } + + /// + /// The Android player, at stripping High and built for ARM64. + /// + /// + /// The project targets ARMv7, which no arm64-only emulator will install + /// (INSTALL_FAILED_NO_MATCHING_ABIS). Set here rather than in ProjectSettings, for the same + /// reason the stripping level is: the run has to prove the strict case without changing what + /// the demo ships with. + /// + public static void Android() + { + PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARM64; + + // ProjectSettings already says com.adaptytest, but a batchmode build does not pick it up - + // it falls back to com.Company.Product. The wrong id is not a build error: the app installs, + // activates and reports success, and only the flow comes back empty, because the backend does + // not recognise it. Set it explicitly so the run measures the real app. + PlayerSettings.SetApplicationIdentifier(NamedBuildTarget.Android, "com.adaptytest"); + + Build(BuildTarget.Android, NamedBuildTarget.Android, "android-stripped-build.apk"); + } + + /// + /// as a development build: a release player's logs do not reach logcat on + /// every device. Stripping stays High. + /// + public static void AndroidDevelopment() + { + PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARM64; + PlayerSettings.SetApplicationIdentifier(NamedBuildTarget.Android, "com.adaptytest"); + + Build( + BuildTarget.Android, + NamedBuildTarget.Android, + "android-dev-build.apk", + BuildOptions.Development + ); + } + + private static void Build( + BuildTarget target, + NamedBuildTarget named, + string output, + BuildOptions options = BuildOptions.None + ) + { + PlayerSettings.SetManagedStrippingLevel(named, ManagedStrippingLevel.High); + PlayerSettings.SetScriptingBackend(named, ScriptingImplementation.IL2CPP); + + Debug.Log( + $"StrippingBuild: {named.TargetName} stripping=" + + PlayerSettings.GetManagedStrippingLevel(named) + + " backend=" + + PlayerSettings.GetScriptingBackend(named) + ); + + var scenes = EditorBuildSettings + .scenes.Where(scene => scene.enabled) + .Select(scene => scene.path) + .ToArray(); + + var report = BuildPipeline.BuildPlayer( + new BuildPlayerOptions + { + scenes = scenes, + locationPathName = output, + target = target, + options = options, + } + ); + + Debug.Log( + $"StrippingBuild: {report.summary.result}, errors: {report.summary.totalErrors}" + + $", size: {report.summary.totalSize}" + ); + + EditorApplication.Exit(report.summary.result == BuildResult.Succeeded ? 0 : 1); + } +} diff --git a/Assets/Editor/StrippingBuild.cs.meta b/Assets/Editor/StrippingBuild.cs.meta new file mode 100644 index 0000000..ff095e2 --- /dev/null +++ b/Assets/Editor/StrippingBuild.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8f9b88552c8f5407f8222c23733db9a0 \ No newline at end of file diff --git a/Assets/Scripts/AdaptyListener.cs b/Assets/Scripts/AdaptyListener.cs index 5b2aaf7..83bc97e 100644 --- a/Assets/Scripts/AdaptyListener.cs +++ b/Assets/Scripts/AdaptyListener.cs @@ -156,7 +156,7 @@ Action completionHandler public void GetPaywallProducts( AdaptyFlow flow, - Action> completionHandler + Action> completionHandler ) { this.LogMethodRequest("GetPaywallProducts"); @@ -185,6 +185,9 @@ Action completionHandler this.LogMethodResult("MakePurchase", error); completionHandler.Invoke(error); + // No result on the error path. + if (error != null) { return; } + switch (result.Type) { case AdaptyPurchaseResultType.Pending: @@ -535,7 +538,7 @@ public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) + details.ToString() ); - this.Router.SetInstallation(new AdaptyInstallationStatusDetermined(details)); + this.Router.SetInstallationDetails(details); } public void OnInstallationDetailsFail(AdaptyError error) @@ -865,7 +868,7 @@ public void FlowViewDidFailLoadingProducts(AdaptyUIFlowView view, AdaptyError er public void FlowViewDidReceiveAnalyticEvent( AdaptyUIFlowView view, string name, - IDictionary @params + IReadOnlyDictionary parameters ) { LogIncomingCall_AdaptyUIFlowView("FlowViewDidReceiveAnalyticEvent", view, name); @@ -876,7 +879,7 @@ IDictionary @params public void FlowViewDidAskPermission( AdaptyUIFlowView view, string permission, - IDictionary customArgs, + IReadOnlyDictionary customArgs, Action respond ) { diff --git a/Assets/Scripts/AdaptyRouter.cs b/Assets/Scripts/AdaptyRouter.cs index 5553f0d..31381e8 100644 --- a/Assets/Scripts/AdaptyRouter.cs +++ b/Assets/Scripts/AdaptyRouter.cs @@ -78,6 +78,14 @@ public void SetInstallation(AdaptyInstallationStatus status) } } + public void SetInstallationDetails(AdaptyInstallationDetails details) + { + if (this.InstallationDetailsSection != null && details != null) + { + this.InstallationDetailsSection.SetInstallationDetails(details); + } + } + public void SetProfile(AdaptyProfile profile) { if (this.ProfileInfoSection != null && profile != null) diff --git a/Assets/Scripts/Flows/FlowsItemView.cs b/Assets/Scripts/Flows/FlowsItemView.cs index ddf21d4..a05f095 100644 --- a/Assets/Scripts/Flows/FlowsItemView.cs +++ b/Assets/Scripts/Flows/FlowsItemView.cs @@ -123,7 +123,7 @@ void LoadProducts(AdaptyFlow flow) ); } - private IEnumerator DelayedUpdateProducts(IList products) + private IEnumerator DelayedUpdateProducts(IReadOnlyList products) { yield return new WaitForEndOfFrame(); this.UpdateProductsData(products); @@ -268,7 +268,7 @@ private void UpdateFlowError(string error) this.ErrorText.SetText("Error: " + error); } - private void UpdateProductsData(IList products) + private void UpdateProductsData(IReadOnlyList products) { // Clear existing product buttons m_productButtons.ForEach( diff --git a/Assets/Scripts/Flows/FlowsListView.cs b/Assets/Scripts/Flows/FlowsListView.cs index 35b7cc1..477d00b 100644 --- a/Assets/Scripts/Flows/FlowsListView.cs +++ b/Assets/Scripts/Flows/FlowsListView.cs @@ -10,6 +10,16 @@ public class FlowsListView : MonoBehaviour public TMP_InputField PlacementIdTextField; + /// + /// Used when the placement field is left empty. + /// + /// + /// The iOS keyboard autocapitalises the first letter, so typing a placement id by hand on + /// a device produces one that does not exist and a fetch failure that looks like an SDK + /// problem. Leaving the field blank uses this instead. + /// + public const string DefaultPlacementId = "calm10"; + /// /// The localization the flow view is built with. A flow itself is not localized at fetch time, /// so this is passed to AdaptyUICreateFlowViewParameters, not to GetFlow. @@ -51,12 +61,9 @@ public void OnDropdownValueChanged(int value) public void AddPlacementPressed() { - if (string.IsNullOrEmpty(this.PlacementIdTextField.text)) - { - return; - } - - var placementId = this.PlacementIdTextField.text; + var placementId = string.IsNullOrEmpty(this.PlacementIdTextField.text) + ? DefaultPlacementId + : this.PlacementIdTextField.text; var placementLocale = this.PlacementLocaleTextField.text; this.AddPlacement(placementId, placementLocale, false); @@ -67,12 +74,9 @@ public void AddPlacementPressed() public void AddPlacementDefaultAudiencePressed() { - if (string.IsNullOrEmpty(this.PlacementIdTextField.text)) - { - return; - } - - var placementId = this.PlacementIdTextField.text; + var placementId = string.IsNullOrEmpty(this.PlacementIdTextField.text) + ? DefaultPlacementId + : this.PlacementIdTextField.text; var placementLocale = this.PlacementLocaleTextField.text; this.AddPlacement(placementId, placementLocale, true); diff --git a/Assets/Scripts/Sections/InstallationDetailsSection.cs b/Assets/Scripts/Sections/InstallationDetailsSection.cs index fc04c72..117e04d 100644 --- a/Assets/Scripts/Sections/InstallationDetailsSection.cs +++ b/Assets/Scripts/Sections/InstallationDetailsSection.cs @@ -16,23 +16,31 @@ public class InstallationDetailsSection : MonoBehaviour public void SetInstallation(AdaptyInstallationStatus status) { - SetStringValue(StatusText, GetStatusName(status)); + SetStringValue(StatusText, GetStatusName(status.Status)); + SetDetails(status.Details); + } - if (status is AdaptyInstallationStatusDetermined determined) - { - var details = determined.Details; - SetStringValue(InstallIdText, ShortenUuid(details.InstallId)); - SetDateValue(InstallTimeText, details.InstallTime); - SetIntegerValue(AppLaunchCountText, details.AppLaunchCount); - SetStringValue(PayloadText, details.Payload); - } - else + public void SetInstallationDetails(AdaptyInstallationDetails details) + { + SetStringValue(StatusText, GetStatusName(AdaptyInstallationStatusType.Determined)); + SetDetails(details); + } + + private void SetDetails(AdaptyInstallationDetails details) + { + if (details == null) { SetNullValue(InstallIdText); SetNullValue(InstallTimeText); SetNullValue(AppLaunchCountText); SetNullValue(PayloadText); + return; } + + SetStringValue(InstallIdText, ShortenUuid(details.InstallId)); + SetDateValue(InstallTimeText, details.InstallTime); + SetIntegerValue(AppLaunchCountText, details.AppLaunchCount); + SetStringValue(PayloadText, details.Payload); } public void GetInstallationDetails() @@ -65,18 +73,18 @@ private string ShortenUuid(string uuid) return uuid; } - private string GetStatusName(AdaptyInstallationStatus status) + private string GetStatusName(AdaptyInstallationStatusType status) { switch (status) { - case AdaptyInstallationStatusNotAvailable: + case AdaptyInstallationStatusType.NotAvailable: return "Not Available"; - case AdaptyInstallationStatusNotDetermined: + case AdaptyInstallationStatusType.NotDetermined: return "Not Determined"; - case AdaptyInstallationStatusDetermined: + case AdaptyInstallationStatusType.Determined: return "Determined"; default: - return status.GetType().Name; + return status.ToString(); } } diff --git a/Assets/Tests.meta b/Assets/Tests.meta new file mode 100644 index 0000000..a06ea7d --- /dev/null +++ b/Assets/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9141611ad9e344c87ac9e59c38dc80d9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/PlayMode.meta b/Assets/Tests/PlayMode.meta new file mode 100644 index 0000000..487749a --- /dev/null +++ b/Assets/Tests/PlayMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5c7bd32dc23324d34b4691aeeae8ad92 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/PlayMode/AdaptyExample.PlayModeTests.asmdef b/Assets/Tests/PlayMode/AdaptyExample.PlayModeTests.asmdef new file mode 100644 index 0000000..b53f36f --- /dev/null +++ b/Assets/Tests/PlayMode/AdaptyExample.PlayModeTests.asmdef @@ -0,0 +1,22 @@ +{ + "name": "AdaptyExample.PlayModeTests", + "rootNamespace": "AdaptyExample.PlayModeTests", + "references": [ + "com.adapty.unity-sdk", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/Tests/PlayMode/AdaptyExample.PlayModeTests.asmdef.meta b/Assets/Tests/PlayMode/AdaptyExample.PlayModeTests.asmdef.meta new file mode 100644 index 0000000..c4631cc --- /dev/null +++ b/Assets/Tests/PlayMode/AdaptyExample.PlayModeTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e01acf7cb8b824ef4aea458169d394c6 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/PlayMode/AdaptyResetOnEnterPlayModeTests.cs b/Assets/Tests/PlayMode/AdaptyResetOnEnterPlayModeTests.cs new file mode 100644 index 0000000..1417756 --- /dev/null +++ b/Assets/Tests/PlayMode/AdaptyResetOnEnterPlayModeTests.cs @@ -0,0 +1,159 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Reflection; +using AdaptySDK; +using NUnit.Framework; +using UnityEngine; + +namespace AdaptyExample.PlayModeTests +{ + /// + /// That Unity really calls the SDK's Play Mode resets. The desktop suite calls them directly, + /// which proves what they do and not that anything invokes them — and an attribute that is + /// never honoured is exactly the failure this guards: the Editor-side callback next to these + /// once had the wrong signature and was dead code that compiled. + /// + /// + /// Only meaningful with Domain Reload disabled, which is why the fixture asserts that first: + /// with it on, the statics are gone because the domain was rebuilt, and the test would pass + /// without the reset existing at all. + /// + [TestFixture] + public class AdaptyResetOnEnterPlayModeTests + { + // Written on the Editor side, by assemblies this one cannot reference. AdaptyPlayModeSeed + // leaves "::<1 if Domain Reload is off>"; AdaptyPlayModeEntryStamp names + // the entry now under way, and wipes the seed's marker on the way into each one. + private const string EntryKey = "adapty.playmode.entry"; + private const string SeedKey = "adapty.playmode.seed"; + + /// + /// How recent the seed's timestamp has to be, once it has already named this entry. + /// + /// + /// A second bound rather than the check itself: it is what remains if the entry id ever + /// stops rotating, which would make a marker from an earlier entry match again. + /// + private static readonly TimeSpan MaxAge = TimeSpan.FromMinutes(5); + + private static readonly string[] Listeners = + { + "m_Listener", + "m_FlowsEventsListener", + "m_SystemRequestsHandler", + "m_ObserverModeResolver", + }; + + private string m_Entry; + private string m_Marker; + + /// + /// Reads the seed's marker and deletes it in the same breath. The entry id is left alone — + /// it belongs to the Editor side, which rotates it per entry. + /// + [OneTimeSetUp] + public void ConsumeTheMarker() + { + m_Entry = PlayerPrefs.GetString(EntryKey, string.Empty); + m_Marker = PlayerPrefs.GetString(SeedKey, string.Empty); + + PlayerPrefs.DeleteKey(SeedKey); + PlayerPrefs.Save(); + } + + [Test] + public void TheSeedRanForThisRun() + { + var age = + DateTime.UtcNow + - new DateTime( + long.Parse(Fields()[1], CultureInfo.InvariantCulture), + DateTimeKind.Utc + ); + + Assert.That( + age, + Is.LessThan(MaxAge), + $"the seed marker is {age.TotalMinutes:F1} minutes old, so the entry id it names is " + + "no longer being rotated and it belongs to an earlier entry" + ); + } + + [Test] + public void DomainReloadIsDisabledForThisRun() + { + Assert.That( + Fields()[2], + Is.EqualTo("1"), + "Domain Reload is on for this run, so the statics were cleared by the domain being " + + "rebuilt and the reset below is not what this measured. Enable Enter Play " + + "Mode Options with Disable Domain Reload." + ); + } + + [Test] + public void UnityClearsEveryListenerOnEnteringPlayMode() + { + var survivors = Listeners + .Where(name => Field(name).GetValue(null) != null) + .ToList(); + + Assert.That( + survivors, + Is.Empty, + "these listeners survived into this run, so the SubsystemRegistration reset did " + + "not run: " + string.Join(", ", survivors) + ); + } + + /// + /// The marker, once it has been shown to belong to this entry. Every test that reads it + /// goes through here, so none of them can report on one another entry left behind. + /// + private string[] Fields() + { + Assert.That( + m_Entry, + Is.Not.Empty, + "no entry id, so AdaptyPlayModeEntryStamp did not run and nothing here can tell " + + "which entry the seed marker belongs to" + ); + + Assert.That( + m_Marker, + Is.Not.Empty, + "AdaptyPlayModeSeed did not run, so the reset had nothing to clear and the " + + "assertions here would hold whatever the SDK does" + ); + + var parts = m_Marker.Split(':'); + + Assert.That( + parts.Length, + Is.EqualTo(3), + $"the seed marker is not ::: \"{m_Marker}\"" + ); + + Assert.That( + parts[0], + Is.EqualTo(m_Entry), + "the seed marker names another entry into Play Mode, so it was left behind by that " + + "one and the seed did not run for this" + ); + + return parts; + } + + private static FieldInfo Field(string name) + { + var field = typeof(Adapty).GetField( + name, + BindingFlags.NonPublic | BindingFlags.Static + ); + + Assert.That(field, Is.Not.Null, $"Adapty.{name} is gone - this test is looking at nothing"); + return field; + } + } +} diff --git a/Assets/Tests/PlayMode/AdaptyResetOnEnterPlayModeTests.cs.meta b/Assets/Tests/PlayMode/AdaptyResetOnEnterPlayModeTests.cs.meta new file mode 100644 index 0000000..f36ce10 --- /dev/null +++ b/Assets/Tests/PlayMode/AdaptyResetOnEnterPlayModeTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e5b9a53d233424895b6ef194def413b2 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 633dabe..9199dfd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,78 +1,8 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +The guidance for this repository lives in [AGENTS.md](AGENTS.md). Read that file. -## Project Overview - -Adapty Unity SDK — a C# wrapper around native [Adapty iOS SDK](https://github.com/adaptyteam/AdaptySDK-iOS) (Swift/SPM) and [Adapty Android SDK](https://github.com/adaptyteam/AdaptySDK-Android) (Kotlin/Maven). Provides in-app purchase management, flow (paywall) rendering, onboarding flows, and subscription analytics for Unity apps. Current SDK version is defined in `Packages/com.adapty.unity-sdk/Runtime/Adapty.cs` (`Adapty.SDKVersion`). - -## Build & Development - -This is a **Unity project** (Unity 6000.x). There is no standalone CLI build or test command — the project is built and tested through the Unity Editor. - -**Build .unitypackage for distribution:** -```bash -cd deploy && ./build_unitypackage.sh # dev mode (keeps Library/) -cd deploy && ./build_unitypackage.sh -p # production (cleans generated files, moves .unitypackage to root) -``` - -**Android wrapper (Java):** Built separately via Gradle in `adaptyandroidwrapper/`: -```bash -cd adaptyandroidwrapper && ./gradlew :unitywrapper:build -``` - -**Native dependency versions:** iOS is declared in `Packages/com.adapty.unity-sdk/Runtime/Editor/AdaptySDKDependencies.xml` (Swift Package Manager via External Dependency Manager 1.2.187+; iOS deployment target 15.0+ is enforced by `Packages/com.adapty.unity-sdk/Editor/AdaptyIOSBuildValidator.cs`). Android is declared in `Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/AdaptySDKDependencies.androidlib/build.gradle`. Update both when bumping native SDK versions. - -## Architecture - -### Cross-Platform Bridge Pattern - -All SDK calls follow a single JSON-based bridge: - -1. **C# public API** (`Packages/com.adapty.unity-sdk/Runtime/Adapty.cs`, `Adapty.Overloads.cs`) — `static partial class Adapty` with methods like `GetFlow`, `MakePurchase`, etc. -2. Each method serializes parameters to JSON via `Request.Send()` (bottom of `Adapty.cs`), which adds the `method` key and calls `_Adapty.Invoke(method, json, callback)`. -3. **`_Adapty`** is compile-time aliased per platform: - - `AdaptySDK.iOS.AdaptyIOS` — P/Invoke `[DllImport("__Internal")]` to Swift plugin - - `AdaptySDK.Android.AdaptyAndroid` — `AndroidJavaClass` calling `com.adapty.unity.AdaptyAndroidWrapper` - - `AdaptySDK.Noop.AdaptyNoop` — no-op for Editor/unsupported platforms -4. Native side processes the JSON request and returns a JSON response string via callback. -5. Response is parsed back into C# models via `+JSON.cs` extension methods. - -### Key Directory Layout - -- **`Packages/com.adapty.unity-sdk/`** — The SDK package distributed to users (UPM layout): - - `Runtime/Adapty.cs` — Main API (all public methods + internal `Request` class) - - `Runtime/Adapty.Overloads.cs` — Convenience overloads with fewer parameters - - `Runtime/IAdaptyEventListener.cs` — Event listener interfaces (`IAdaptyEventListener`, `IAdaptyFlowsEventsListener`, `IAdaptyUISystemRequestsHandler`, `IAdaptyUIObserverModeResolver`, `IAdaptyOnboardingsEventsListener`) and the `OnMessage` dispatcher - - `Runtime/Models/` — C# data models (one file per type, e.g. `AdaptyFlow.cs`) - - `Runtime/JSON/` — JSON serialization/deserialization extensions (one `+JSON.cs` per model, plus `SimpleJSON.cs` library) - - `Runtime/Plugins/iOS/` — `AdaptyIOS.cs` (P/Invoke bridge) + `Source/` (Swift/ObjC native plugin code) - - `Runtime/Plugins/Android/` — `AdaptyAndroid.cs` (JNI bridge) + `Local/` (local AAR maven repo) + `AdaptySDKDependencies.androidlib` (Android maven dependencies) - - `Runtime/Plugins/AdaptyNoop.cs` — Editor/no-op stub - - `Runtime/Editor/AdaptySDKDependencies.xml` — iOS Swift Package declaration for External Dependency Manager - - `Editor/` — Editor-only assembly (iOS build validation) -- **`adaptyandroidwrapper/`** — Standalone Android Gradle project: - - `unitywrapper/src/main/java/com/adapty/unity/` — `AdaptyAndroidWrapper.java` (entry point), callback handler, message handler -- **`Assets/Scripts/`** — Demo app scripts (not part of distributed SDK) -- **`cross_platform.yaml`** — Cross-platform API contract schema defining all request/response JSON formats and data types shared across iOS/Android/Unity - -### Event System - -Native SDKs push events (profile updates, flow view lifecycle, onboarding events) via the same JSON bridge. `Adapty.OnMessage(id, json)` in `IAdaptyEventListener.cs` dispatches by event `id` string to the registered listener interfaces. Two event families are round-trips: flow permission requests are answered via `flow_view_did_answer_permission` (keyed by `event_id`), and Observer-mode purchases/restores report back via `observer_*_did_start/finish`. - -### Model + JSON Convention - -Each model has two files: -- `Runtime/Models/AdaptyFoo.cs` — C# class/struct definition -- `Runtime/JSON/AdaptyFoo+JSON.cs` — `ToJSONNode()` serialization and `GetAdaptyFoo()` deserialization extension methods - -When adding a new model, create both files following this pattern. The JSON keys must match `cross_platform.yaml` definitions, including which fields are required vs optional (optional fields parse with `*IfPresent` accessors). - -## Version Bumping - -When releasing a new version, update: -1. `Adapty.SDKVersion` in `Packages/com.adapty.unity-sdk/Runtime/Adapty.cs` -2. `version` in `Packages/com.adapty.unity-sdk/package.json` -3. Native dependency versions: iOS in `Runtime/Editor/AdaptySDKDependencies.xml`, Android in `Runtime/Plugins/Android/AdaptySDKDependencies.androidlib/build.gradle` and `adaptyandroidwrapper/unitywrapper/build.gradle` (then rebuild the AAR into `Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper//`) -4. `cross_platform.yaml` schema `$id` version — must match the canonical contract in AdaptySDK-iOS (`Sources.AdaptyPlugin/cross_platform.yaml`); diff the two files, not just the version -5. `CHANGELOG.md` and the `_upm.changelog` string in `package.json` — keep both in sync, the latter is what Package Manager shows after an update +Nothing is duplicated here on purpose: two copies of a rule drift apart, and the one you happened +to open is then as likely to be the stale one. This file is a pointer, not a summary, and it is +deliberately not a symlink — a symlink is easy to follow by accident and hard to notice when it +breaks. diff --git a/MIGRATION-v3.17-to-v4.0.md b/MIGRATION-v3.17-to-v4.0.md new file mode 100644 index 0000000..ee785eb --- /dev/null +++ b/MIGRATION-v3.17-to-v4.0.md @@ -0,0 +1,294 @@ +# Migrate Adapty Unity SDK to v4.0 + +v4.0 introduces flows and renames the paywall APIs accordingly. The new APIs work with both the new +Flow Builder and the existing Paywall Builder, and nothing changes on the Adapty Dashboard side. + +This guide is the move from v3.17 to v4.0. Read **Before you upgrade** first and sort its +prerequisites by when they bite: Unity and Newtonsoft.Json have to be in place before your C# +compiles at all, while External Dependency Manager, Xcode and the iOS deployment target are only +needed by the time you build for iOS. The other sections are independent of each other; take them +in whatever order suits your project. Everything +this guide does not cover — why each change was made, 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 paywall APIs to flows](#rename-the-paywall-apis-to-flows) +3. [Update listeners and handlers](#update-listeners-and-handlers) +4. [Fix the compile errors](#fix-the-compile-errors) +5. [Review the runtime behavior changes](#review-the-runtime-behavior-changes) +6. [Optional](#optional) + +## Before you upgrade + +**Unity 2022.3 or later**, and two packages: + +| Package | Comes from | Installed for you | Needed by | +|---|---|---|---| +| `com.unity.nuget.newtonsoft-json` 3.2.2 | Unity registry | Yes, with Package Manager | compile time — the SDK assembly is gated on it | +| `com.google.external-dependency-manager` 1.2.188 | OpenUPM | No — a peer dependency, as in v3 | iOS build — it resolves the Swift package. Android does not go through it | + +Newtonsoft.Json replaces the JSON parser that used to ship inside the SDK, so it is new in v4.0. One +menu item installs whichever is missing, adds the OpenUPM registry, and upgrades an External +Dependency Manager below 1.2.188 — v3 declared 1.2.187, so a project coming from it has one: + +> **Adapty SDK > Install Dependencies** + +A copy of External Dependency Manager installed from Google's own `.unitypackage` under `Assets/` +has no version Package Manager can read. It is left alone with a warning, and you update it +yourself. + +**Installing from a `.unitypackage`: add `com.unity.nuget.newtonsoft-json` before you import.** A +`.unitypackage` carries assets only and cannot touch your project manifest. Without Newtonsoft the +SDK assembly is skipped, your calls into Adapty stop compiling (`error CS0103: The name 'Adapty' +does not exist in the current context`), and the menu item above is unavailable — Unity does not +load the Editor assembly it lives in while your scripts fail to compile. Recover through **Window > +Package Manager > + > Add package by name**. It has to be that package: a `Newtonsoft.Json.dll` +dropped into `Assets/` does not satisfy the SDK, and installing the package on top of one leaves two +copies. Delete the DLL first. + +**Installing from a `.unitypackage`: delete `Assets/AdaptySDK` before you import.** A +`.unitypackage` never removes files, and 4.0 drops 62 sources that 3.17 shipped — the whole +`Assets/AdaptySDK/JSON/` folder, plus `AdaptyPaywall.cs` and its neighbours. Importing over them +keeps the folder and the assembly definition GUIDs, so the leftovers compile into the same assembly +as the new sources: 35 of them declare a `partial` half of a type the new sources also declare — +`AdaptyPlacement`, `AdaptyProfile`, `AdaptyPaywallProduct` among them, and 4.0 declares most of +those `sealed` — while the rest call constructors and a `SimpleJSON` namespace that are gone. The +errors do not appear at import — the assembly is gated on Newtonsoft — but the +moment Newtonsoft is in place the project stops compiling, and from there the menu item above is out +of reach too. Deleting the folder first costs nothing: everything in it is replaced. + +**iOS requirements changed — needed to build for iOS, not to compile.** Nothing below blocks the +rest of this guide; the deployment target is checked by a build validator when an iOS build starts, +and Xcode only comes in after the export, when it resolves the Swift package. + +| | v3 | v4 | +|---|---|---| +| Xcode | any recent | **26 or later** | +| Deployment target | 13.0 | **15.0 or later**, enforced by a build validator | +| Native dependency | CocoaPods (`iosPods`) | Swift Package Manager | + +Stop running **Assets > External Dependency Manager > iOS Resolver > Install Cocoapods** for Adapty; +no pod of ours appears in the Podfile any more. **Keep building +`Unity-iPhone.xcworkspace`,** exactly as in v3 — External Dependency Manager still generates it and +still wires `Pods_UnityFramework` into the Unity target, so building `Unity-iPhone.xcodeproj` +directly fails with `ld: framework 'Pods_UnityFramework' not found`. + +Android needs nothing from you: the dependencies are declared in an `.androidlib` module Unity +includes in the Gradle build on its own. + +## Rename the paywall APIs to flows + +`AdaptyPaywall` becomes `AdaptyFlow`, and a flow is not a paywall — it holds the paywall variations. + +```csharp +- Adapty.GetPaywall("YOUR_PLACEMENT_ID", "en", (paywall, error) => { }); ++ Adapty.GetFlow("YOUR_PLACEMENT_ID", (flow, error) => { }); + +- AdaptyUI.CreatePaywallView(paywall, parameters, (view, error) => { }); ++ AdaptyUI.CreateFlowView(flow, parameters, (view, error) => { }); + +- AdaptyUI.PresentPaywallView(view, (error) => { }); ++ AdaptyUI.PresentFlowView(view, (error) => { }); + +- AdaptyUI.DismissPaywallView(view, (error) => { }); ++ AdaptyUI.DismissFlowView(view, (error) => { }); +``` + +| v3 | v4 | +|---|---| +| `Adapty.GetPaywall` | `Adapty.GetFlow` — **no locale argument**; it moved to `AdaptyUICreateFlowViewParameters.Locale`, since a flow is localized when its view is built | +| `Adapty.GetPaywallForDefaultAudience` | `Adapty.GetFlowForDefaultAudience` | +| `Adapty.GetPaywallProducts(paywall, ...)` | same name, takes an `AdaptyFlow` | +| `Adapty.LogShowPaywall` | `Adapty.LogShowFlow`, takes an `AdaptyFlow`. Same variation, so funnels and A/B tests carry over | +| `AdaptyUIPaywallView` | `AdaptyUIFlowView` | +| `view.PaywallVariationId` | `view.VariationId` — same value, shorter name now that the view is a flow's. The view also gains `view.Locale`, the localization it was built with | +| `AdaptyUICreatePaywallViewParameters` | `AdaptyUICreateFlowViewParameters` — same fields, plus `Locale` and `EnableSafeAreaPaddings` (Android only, defaults to `true`) | +| `paywall.RemoteConfig` | `flow.RemoteConfigs`, one per configured language. `RemoteConfig` still exists and returns the first | +| `paywall.Products` | `flow.ProductIdentifiers`, or `GetPaywallProducts(flow, ...)` | +| `paywall.HasViewConfiguration` | removed — `CreateFlowView` returns an error instead | +| — | `flow.Paywalls`, the paywall variations; `flow.FlowVersionId`, nullable | +| — | `AdaptyUI.OpenUrl` and `AdaptyUI.RequestAppReview` are new | + +`Placement`, `InstanceIdentity`, `Name`, `VariationId`, `ProductIdentifiers` and `VendorProductIds` +keep their names on `AdaptyFlow`. `AdaptyPaywallProduct` keeps its name and gains `FlowProductId`, +nullable. + +The members deprecated in v3.14 are gone with the type: + +| Removed from `AdaptyPaywall` | Use instead | +|---|---| +| `PlacementId`, `AudienceName`, `ABTestName`, `Revision` | `flow.Placement.Id`, `.AudienceName`, `.ABTestName`, `.Revision` | +| `RemoteConfigString`, `Locale` | `flow.RemoteConfig.Data`, `flow.RemoteConfig.Locale` | + +### Product references have no replacement + +`paywall.Products` and the public `AdaptyProductReference` are both gone. +`AdaptyFlowPaywall.ProductReference` is not that type renamed — it is internal, and no public member +returns one, so you cannot write that code. Migrate by what you read the reference for: + +| You needed | In v4 | +|---|---| +| The product ids of a flow | `flow.ProductIdentifiers` or `flow.VendorProductIds` | +| The products themselves | `Adapty.GetPaywallProducts(flow, ...)`, giving `AdaptyPaywallProduct` | +| Access level, product type | `product.AccessLevelId`, `product.ProductType` | +| Offer id and kind | `product.Subscription.Offer.Identifier` and `.Type` | +| Android base plan | `product.Subscription.BasePlanId`, or `identifier.BasePlanId` | + +`AdaptyProductIdentifier` carries `VendorProductId` and `BasePlanId` only; anything else comes from +the fetched product. + +### Web paywalls take one variation + +`CreateWebPaywallUrl` and `OpenWebPaywall` took an `AdaptyPaywall` and now take an +`AdaptyFlowPaywall` — one variation out of the flow, not the flow itself. Pick the one you mean: + +```csharp +- AdaptyPaywall paywall = ...; ++ AdaptyFlowPaywall paywall = flow.Paywalls[index]; + + Adapty.CreateWebPaywallUrl(paywall, (url, error) => { }); +``` + +The overloads taking an `AdaptyPaywallProduct` are unchanged. + +## Update listeners and handlers + +The interfaces carry the C# `I` prefix, and the paywall one is about flows: + +```csharp +- public class MyListener : AdaptyEventListener, AdaptyPaywallsEventsListener ++ public class MyListener : IAdaptyEventListener, IAdaptyFlowsEventsListener +``` + +| v3 | v4 | +|---|---| +| `AdaptyEventListener` | `IAdaptyEventListener` | +| `AdaptyPaywallsEventsListener` | `IAdaptyFlowsEventsListener` | +| `AdaptyOnboardingsEventsListener` | `IAdaptyOnboardingsEventsListener` | +| `Adapty.SetPaywallsEventsListener` | `Adapty.SetFlowsEventsListener` | +| `void PaywallViewDid…(AdaptyUIPaywallView view, …)` | `void FlowViewDid…(AdaptyUIFlowView view, …)` — every callback | +| `PaywallViewDidFailRendering` | `FlowViewDidReceiveError`, which also fires for other runtime errors | + +Two handler interfaces are new, each with two callbacks you implement, plus one new callback on the +flows listener: + +| Interface | Registered with | Callbacks | +|---|---|---| +| `IAdaptyUISystemRequestsHandler` | `Adapty.SetSystemRequestsHandler` | `FlowViewDidAskPermission`, `FlowViewDidRequestAppReview` | +| `IAdaptyUIObserverModeResolver` | `Adapty.SetObserverModeResolver` | `FlowViewDidInitiatePurchase`, `FlowViewDidInitiateRestore` | +| `IAdaptyFlowsEventsListener` | `Adapty.SetFlowsEventsListener` | `FlowViewDidReceiveAnalyticEvent` | + +Three rules about the new callbacks: + +- **Answer a permission request exactly once.** Until `respond` runs the flow stays pending; + dismissing the view resolves it as denied. +- **`FlowViewDidRequestAppReview` must call `AdaptyUI.RequestAppReview`** to keep the default + behavior. An empty body is worse than registering no handler, because with no handler the SDK + makes that call for you. +- **Implement `IAdaptyUIObserverModeResolver` only if you run in Observer mode.** + +`FlowViewDidReceiveAnalyticEvent` is the one you may leave empty — it is a live event you are +dropping, not a placeholder. + +## Fix the compile errors + +Renames, first: + +| v3 | v4 | +|---|---| +| `Adapty.SetFallbackPaywalls` | `Adapty.SetFallback` | +| `builder.SetIDFACollectionDisabled` | `builder.SetAppleIDFACollectionDisabled` | +| `Adapty.GetLoglevel` | `Adapty.GetLogLevel` — a typo fixed, so there was no v3 warning for this one | +| `builder.IdfaCollectionDisabled` | `builder.AppleIdfaCollectionDisabled` — the property matching the method above. It was already `[Obsolete]` in v3, naming this same replacement | + +Removed with a replacement: + +| Removed | Use instead | +|---|---| +| `AdaptyProfile.NonSubscription.IsOneTime` | `IsConsumable`, which it returned unchanged | +| `AdaptyPlacement.GetIsTrackingPurchases` | `IsTrackingPurchases`, the field it wrapped — but that field is `bool?`, where the removed member returned `bool`. Write `IsTrackingPurchases ?? false` to keep the old expression's type | +| `AdaptyInstallationStatusNotAvailable`, `AdaptyInstallationStatusNotDetermined`, `AdaptyInstallationStatusDetermined` | `AdaptyInstallationStatus.Status` and `.Details`, see below | +| `AdaptyErrorCode.PendingPurchase` (25) | `AdaptyPurchaseResultType.Pending`, see below | +| `AdaptyErrorCode.InvalidJson` (23) | nothing — no native SDK can raise it | +| The `AdaptySDK.SimpleJSON` namespace, and the `ToJSONNode` extension classes `AdaptyRefundPreferenceExtensions`, `AdaptyUIIOSPresentationStyleExtensions`, `AdaptyUIOnboardingMetaExtensions`, `AdaptyWebPresentationExtensions` | Newtonsoft.Json, now a dependency of the package and available to your assemblies | + +Changed types and shapes: + +| Member | Change | +|---|---| +| Collections on `AdaptyProfile`, `AdaptyFlow`, `AdaptyFlowPaywall`, `AdaptySubscriptionOffer`, `AdaptyRemoteConfig`, and the `GetPaywallProducts` callback | `IList` → `IReadOnlyList`, `IDictionary` → `IReadOnlyDictionary`. `AdaptyProfile.NonSubscriptions` is read-only at both levels | +| The four `AdaptyUICreateFlowViewParameters` setters and `UpdateAttribution` | take an `IReadOnlyDictionary` and **copy** it, so filling your dictionary afterwards no longer changes the view. The four matching members are read-only properties — assign through the setters | +| `AdaptyProfileParameters.CustomAttributes` | was a `Dictionary` you could write into and is now a read-only view over the builder's own storage. There is no setter taking a dictionary: use `SetCustomStringAttribute`, `SetCustomDoubleAttribute` and `RemoveCustomAttribute`. Copying this one does not reach the request | +| `FlowViewDidReceiveAnalyticEvent`, `FlowViewDidAskPermission` | take `IReadOnlyDictionary` instead of `IDictionary`. The analytics parameter is renamed `@params` → `parameters`, which matters only for a named argument | +| Every concrete public class | `sealed`. If you derived from one, hold it as a field instead of inheriting it | +| `AdaptyPlacementFetchPolicy.Default`, `.ReloadRevalidatingCacheData`, `.ReturnCacheDataElseLoad` | `readonly` — reading is unchanged, assigning no longer compiles | +| `AdaptyConfiguration.Builder.ServerCluster` | `AdaptyServerCluster?` where it was `AdaptyServerCluster`. `SetServerCluster` is unchanged | + +Reading a read-only collection is unaffected — `foreach` and LINQ included. Writing needs a copy +first, which `ToDictionary` from `System.Linq` does in one line: + +```csharp +- profile.CustomAttributes["seen_intro"] = true; ++ var attributes = profile.CustomAttributes.ToDictionary(pair => pair.Key, pair => pair.Value); ++ attributes["seen_intro"] = true; +``` + +`AdaptyInstallationStatus` is one sealed type instead of a base class and three subclasses. +`GetCurrentInstallationStatus` still hands back an `AdaptyInstallationStatus`; you switch on its +`Status`, and `Details` is non-null exactly when that is `Determined`: + +```csharp +- if (status is AdaptyInstallationStatusDetermined determined) +- { +- Debug.Log(determined.Details.InstallId); +- } ++ if (status.Status == AdaptyInstallationStatusType.Determined) ++ { ++ Debug.Log(status.Details.InstallId); ++ } +``` + +A pending purchase is a result rather than an error, so the check moves to the other branch of the +callback — on the error path the result is null: + +```csharp +Adapty.MakePurchase(product, (result, error) => +{ + if (error != null) + { + // AdaptyErrorCode.PendingPurchase used to be checked here. + return; + } + + if (result.Type == AdaptyPurchaseResultType.Pending) { /* ... */ } +}); +``` + +## Review the runtime behavior changes + +These compile as they are and behave differently at runtime, so the compiler will not point them +out: + +- **A flow view stays open after a purchase.** Dismiss it yourself from `FlowViewDidFinishPurchase` + when that is what you want. +- **A view is single use.** After `DismissFlowView` it is destroyed; call `CreateFlowView` again to + show the flow again. +- **The Android system back button no longer closes the view on its own.** It arrives in + `FlowViewDidPerformAction` as a `SystemBack` action, which is what iOS already did. +- **`AdaptySubscriptionOfferType` gained `Code`** (iOS only). Existing members keep their values, but + a `switch` that was exhaustive in v3 is not exhaustive now — give it a default branch. + +## Optional + +**Remove workarounds you no longer need.** Three v3 defects are fixed, so code written around them +can go: `ReportTransaction` no longer reports a decoding error on success, `AdaptyProductIdentifier` +compares by value so identifiers from a flow work as dictionary keys, and a call in the Editor +returns a readable "not supported on this platform" error instead of a null one that looked like +success. The full list of fixes is in the changelog. + +**Move off the legacy onboarding API.** `GetOnboarding`, `AdaptyUI.CreateOnboardingView` and the +rest still work and now warn at compile time. Build onboardings as flows instead. + +**Kids Mode.** If your app ships in the App Store Kids Category, v4.0 adds the `ADAPTY_KIDS_MODE` +scripting define, which compiles IDFA, AdSupport and AppTrackingTransparency out of the iOS binary. +See the [README](README.md#kids-mode-on-ios) for how to set it. diff --git a/Packages/com.adapty.unity-sdk/CHANGELOG.md b/Packages/com.adapty.unity-sdk/CHANGELOG.md index 7983e10..9ef94ff 100644 --- a/Packages/com.adapty.unity-sdk/CHANGELOG.md +++ b/Packages/com.adapty.unity-sdk/CHANGELOG.md @@ -4,15 +4,23 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [4.0.0] - 2026-07-29 +## [4.0.0-beta.2] - 2026-08-15 + +Upgrading from 3.x: see [MIGRATION-v3.17-to-v4.0.md](https://github.com/adaptyteam/AdaptySDK-Unity/blob/4.0.0-beta.2/MIGRATION-v3.17-to-v4.0.md). +If you install from a `.unitypackage`, delete `Assets/AdaptySDK` and add +`com.unity.nuget.newtonsoft-json` **before** importing. A `.unitypackage` never removes files, so the +62 sources this release drops would otherwise stay behind and compile alongside the new ones, which +they collide with. Until Newtonsoft is there the SDK assembly is skipped, so code that calls Adapty +will not compile — and once it does not, Unity stops loading the Editor assembly the installer menu +lives in. ### Added - **iOS Kids Mode** for the App Store Kids Category / COPPA. Setting the `ADAPTY_KIDS_MODE` scripting define enables the `KidsMode` trait on the AdaptySDK-iOS Swift package, so IDFA, AdSupport and AppTrackingTransparency are compiled out of the binary, and forces - `apple_idfa_collection_disabled` in the runtime configuration. Requires Xcode 26 or newer - (Swift package traits). Set the define in Player Settings; a build profile's scripting defines + `apple_idfa_collection_disabled` in the runtime configuration. Swift package traits need Xcode 26, + which v4 requires anyway. Set the define in Player Settings; a build profile's scripting defines work too, but only once Unity has recompiled the Editor assemblies for them, and the iOS build fails if the SDK detects that it is running against stale ones. `BuildPlayerOptions.extraScriptingDefines` is not supported at all, because it reaches the player @@ -26,29 +34,251 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 that does not report it. - `AdaptyUICreateFlowViewParameters.EnableSafeAreaPaddings` (Android only) — lays the flow view out without safe area paddings when set to `false`. +- **Adapty SDK > Install Dependencies** — installs whichever of the SDK's package dependencies are + missing: Newtonsoft.Json, and External Dependency Manager along with the OpenUPM scoped registry it + is published on. A `.unitypackage` carries assets only and can bring neither, and External + Dependency Manager has always had to be installed by hand even alongside Package Manager. Packages + already in the project are left as they are, apart from an External Dependency Manager older than + the SDK needs, which is upgraded. +- `AdaptyErrorCode.NoPurchasesToRestore` (1004) — restored. The member was commented out in December + 2024 while the native Android SDK kept sending the code, so `RestorePurchases` on a profile with + nothing to restore returned an error that could only be matched against a literal `1004`. Nothing + about the error changes; it now has its name back. The code is Android-only — iOS does not define + it. +- Seven more `AdaptyErrorCode` members the native SDKs declare but this enum never named: + `UnidentifiedUserLogout` (3020, both platforms, from `Logout` on an unidentified profile), + `PaymentPendingError` (1050, iOS), `BillingNetworkError` (112, Android), and `WrongAssetType` + (4104), `JsException` (4105), `NavigatorNotFound` (4106), `InvalidActionUrl` (4107) — the four + the Android flow renderer reports through `FlowViewDidReceiveError`. `AdaptyErrorCode` carries + the native number, so these codes already arrived; they simply had no constant to match against. + Each one was traced in the iOS 4.0.2 and Android 4.0.1 sources to the place the native SDK + produces it — a throw site for all but 112, which comes out of the `fromBilling` mapping. + + `PaymentPendingError` is the one exception to "already arrived", and is named for completeness + rather than to be handled: its only throw site is an iOS overload taking StoreKit's own purchase + result, which the Unity bridge never calls — `ReportTransaction` goes to the overload taking a + transaction id, which has no pending branch. A pending purchase made through the SDK arrives as + `AdaptyPurchaseResultType.Pending`, on the result rather than the error. ### Changed +- **The minimum supported Unity version is now declared: 2022.3.** It was never stated before, in + `package.json` or anywhere else, so Package Manager let any Editor install a package it might not + be able to compile. Nothing was dropped — the floor is now stated. + + Installing on the floor is verified: a clean `.unitypackage` import and + **Adapty SDK > Install Dependencies** were run end to end on 2022.3, and the SDK compiles + afterwards. Player builds, device runs and the rest of the acceptance matrix were done on Unity 6, + which is what the SDK is developed against. +- **The JSON layer now uses Newtonsoft.Json instead of the bundled SimpleJSON.** The package depends + on `com.unity.nuget.newtonsoft-json` 3.2.2, which Package Manager installs for you and + **Adapty SDK > Install Dependencies** installs for everyone else. While Newtonsoft is absent the + SDK assembly is skipped by a define constraint instead of failing to compile. The SDK reports the + reason in the Editor console — as long as its Editor assembly loads, which it does not while your + own scripts fail to compile. A second copy of Newtonsoft is reported the same way, since it makes + its types ambiguous. + The models, their members and every method signature are unchanged, so calling code is unaffected. + **Breaking for anything that used `AdaptySDK.SimpleJSON` directly:** the namespace is gone, and its + public types (`JSON`, `JSONNode`, `JSONObject`, `JSONArray` and the rest) went with it. - **Breaking:** migrated the paywall API to flows — `AdaptyPaywall` → `AdaptyFlow`, `GetPaywall` → `GetFlow`, `CreatePaywallView` → `CreateFlowView`, and the corresponding models, events and view controllers. Event listener interfaces now carry the `I` prefix (`IAdaptyEventListener`, `IAdaptyFlowsEventsListener`, `IAdaptyUISystemRequestsHandler`, `IAdaptyUIObserverModeResolver`, `IAdaptyOnboardingsEventsListener`). -- The legacy onboarding API is deprecated in favor of flows. +- The legacy onboarding API is deprecated in favor of flows, and `[Obsolete]` now covers the whole + of it rather than only its entry points: `IAdaptyOnboardingsEventsListener`, `AdaptyOnboarding`, + `AdaptyUIOnboardingView`, `AdaptyUIOnboardingMeta`, the `AdaptyOnboardingsAnalyticsEvent`, + `AdaptyOnboardingsStateUpdatedParams` and `AdaptyOnboardingsInput` hierarchies, and the + `AdaptyUI.ShowDialog` overload taking an `AdaptyUIOnboardingView`. Naming any of them warns now, + where before only calling one of the six entry points did. +- **Breaking:** `Adapty.GetLoglevel` is spelled `Adapty.GetLogLevel`. The typo was in the v3 surface + too, out of step with its own `SetLogLevel` and with `get_log_level`, the operation the + cross-platform contract names. Nothing else changes — same signature, same wire method. +- **Breaking:** removed two members that only forwarded to another one — + `AdaptyProfile.NonSubscription.IsOneTime` (returned `IsConsumable` unchanged; its summary had + called it deprecated for several versions without an attribute to back that up) and + `AdaptyPlacement.GetIsTrackingPurchases` (wrapped the public `IsTrackingPurchases` field, whose + `null` case cannot occur). +- **Breaking:** a string the contract does not list now fails the read instead of degrading to + `Unknown`, and the six members that existed only to catch one are gone — + `AdaptyPurchaseResultType.Unknown`, `AdaptySubscriptionOfferType.Unknown`, + `AdaptySubscriptionRenewalType.Unknown`, `AdaptyUIDialogActionType.Unknown`, + `AdaptyUIUserActionType.Unknown` and `AdaptyWebPresentation.Unknown`. The SDK ships pinned to the + native SDKs it is built against, so an unlisted value is a broken payload rather than one from the + future, which is what v3 did too; the fallback was carried over from the beta and the contract + never allowed an arbitrary string in these positions. `AdaptyPaymentMode.Unknown` and + `AdaptySubscriptionPeriodUnit.Unknown` stay, because the contract lists `"unknown"` among their + values. No surviving member changed its numeric value. `AdaptySubscriptionOfferType.Unknown` was + also the one fallback that could be sent — as `"unknown"`, which no branch of the contract's offer + identifier accepts — so a purchase of such an offer failed on the native side instead of here. + A JSON number is no longer accepted for a string enum either; it used to read as `Unknown`. +- **iOS builds now require Xcode 26 or newer.** AdaptySDK-iOS 4.0 declares + `swift-tools-version: 6.2`, where the 3.17.2 that v3 pinned declared 6.0, and Swift Package Manager + refuses a package whose tools version is newer than the installed toolchain. On Xcode 16 the build + fails while resolving the dependency, before anything is compiled. This is the floor for the whole + SDK, not only for Kids Mode. Nothing in Unity can check it — the Editor never sees which Xcode will + open the generated project. - Updated the cross-platform contract to 4.0.2 and the native SDK dependencies to iOS 4.0.2 and Android 4.0.1. `MakePurchase`'s purchase parameters and `AdaptyUICreateFlowViewParameters.ProductPurchaseParameters` are now documented as Android only, matching what the native SDKs actually do with them. - Errors returned by `flow_view_did_answer_permission` and by the observer-mode round trips are now logged instead of being swallowed. +- **Breaking:** removed `AdaptyErrorCode.InvalidJson` (23) and `AdaptyErrorCode.PendingPurchase` + (25). Neither native SDK has these codes: iOS 4.0 declares no 23 or 25 at all, and the Android + enum runs 20, 22, 24, 97 — the two numbers are gaps left where the members were deleted, while + their neighbours stayed. Nothing can raise them, so nothing can match on them; a pending + purchase is reported as `AdaptyPurchaseResultType.Pending` rather than as an error. Removing a + public member is a breaking change, which is why it happens in a major. +- **Breaking:** removed what the old JSON layer left behind. + `AdaptyRefundPreferenceExtensions.ToJSONNode` was the last of the `ToJSONNode` extension classes — + the other three went with `AdaptySDK.SimpleJSON`, while this one survived because it sat in + `Models/`. The SDK does not call it: the refund preference is serialized through its + `[EnumMember]` mapping like every other enum. Two constructors that only the hand-written parser + ever called are gone too, though those were never public. +- **Breaking:** the collections on a response model are read-only. `AdaptyProfile`, `AdaptyFlow`, + `AdaptyFlowPaywall`, `AdaptySubscriptionOffer` and `AdaptyRemoteConfig` hand back + `IReadOnlyList` and `IReadOnlyDictionary` instead of `IList` and `IDictionary`, + and `AdaptyProfile.NonSubscriptions` is read-only at both levels. A `readonly` field never made + these models immutable: the reference could not be replaced, but the contents could, and the SDK + handed out its own storage. The views refuse to write — casting one back to `IDictionary` still + compiles, because `ReadOnlyDictionary` implements it, and every mutating call throws + `NotSupportedException`. `GetPaywallProducts` reports an `IReadOnlyList` for + the same reason. The deprecated onboarding API is the exception and keeps its old shapes — + `AdaptyOnboardingsMultiSelectParams.Params` is still an `IList` handed over as it was received. + It is maintained rather than improved until it is removed, so do not read the sentence above as + covering it. +- **Breaking:** the parameter objects take the narrowest abstraction and copy it. + `AdaptyUICreateFlowViewParameters.SetCustomTags`, `SetCustomTimers`, `SetCustomAssets` and + `SetProductPurchaseParameters` accept an `IReadOnlyDictionary` and copy it, so a caller that keeps + writing to its own dictionary afterwards no longer changes what the view is built with; the four + matching members are now read-only properties rather than public fields. `UpdateAttribution` takes + an `IReadOnlyDictionary`, and `AdaptyProfileParameters.CustomAttributes` exposes a + view rather than the dictionary the builder writes into. +- **Breaking, at the call site only:** the analytics-event parameter of + `IAdaptyFlowsEventsListener.FlowViewDidReceiveAnalyticEvent` is named `parameters` rather than + `@params`. The CLR signature is unchanged, so an implementation keeps compiling and only a call + passing it as a named argument — `@params:` — has to be renamed. The deprecated onboarding + listener keeps its own `@params`. +- **Breaking:** `IAdaptyFlowsEventsListener.FlowViewDidReceiveAnalyticEvent` and + `IAdaptyUISystemRequestsHandler.FlowViewDidAskPermission` receive `IReadOnlyDictionary` instead of + `IDictionary`. Implementations need the signature updated; nothing else about them changes. +- **Breaking:** `AdaptyPlacementFetchPolicy.Default`, `.ReloadRevalidatingCacheData` and + `.ReturnCacheDataElseLoad` are `readonly`. They were public mutable statics, so any code could + repoint the SDK's shared defaults for every other caller — and with Domain Reload disabled the + change outlived Play Mode. +- Registered listeners no longer survive Play Mode. With Domain Reload disabled — the default for + fast iteration — Unity keeps static fields between runs, so the event listener, flows listener, + system request handler and observer-mode resolver a previous run registered were still there for + the next one, which then delivered its events to objects belonging to a session that had ended. + The SDK now clears them, and the no-op bridge's test hook with them, at + `RuntimeInitializeLoadType.SubsystemRegistration`. Call `SetEventListener` and friends on start as + you always should; nothing changes when Domain Reload is on. +- **Breaking:** every concrete public class is now `sealed`; the four abstract roots the wire + contract needs — `AdaptyCustomAsset` and the three legacy onboarding hierarchies — stay open. For + most of them this states what was already true: a response model has no constructor reachable from + outside the SDK — private, or `internal` as on `AdaptySubscriptionOffer` — so no type of yours + could derive from one in the first place. Eleven could, all of them inputs rather than responses — + the parameter objects, the two identity types, the three builders — and for those this is a real + restriction. Nothing was designed for extension: no model declares a + `virtual` or `protected` member, and a subclass of a parameter object would have been a trap, + since the SDK serializes the declared contract and silently drops whatever the subclass added. + `AdaptyProductIdentifier` is the one where it also fixes something: it compares by value and is + used as a dictionary key, and a subclass would have broken the symmetry of `Equals`. +- **Breaking:** `AdaptyInstallationStatus` is one sealed type carrying a `Status` and a `Details`, + instead of a base class and the three subclasses `AdaptyInstallationStatusNotAvailable`, + `AdaptyInstallationStatusNotDetermined` and `AdaptyInstallationStatusDetermined`, which are gone + along with their public constructors — the type is a response, and only the SDK builds one. The + new `AdaptyInstallationStatusType` names the same three states the contract lists, so a caller + switches on a value rather than testing for a type. `Details` is non-null exactly when `Status` is + `Determined`: the determined branch is rejected without it, and on the other two branches a stray + one is dropped, which is what the removed subclasses did with it. Nothing about the wire format + changes, and the polymorphic converter the hierarchy needed is gone with it. +- **Breaking:** `AdaptyFlowPaywall.ProductReference` is now `internal`. Its constructor was private + and every one of its members was already `internal`, so no instance of it could be obtained or + read from outside the SDK; the type was public only because in 3.x it was the top-level + `AdaptyProductReference`. ### Fixed +- The `respond` delegate of `IAdaptyUISystemRequestsHandler.FlowViewDidAskPermission` and the report + callbacks of `IAdaptyUIObserverModeResolver` are now safe to invoke from any thread, which is where + they are invoked from in practice — an OS permission callback, a billing implementation's own + thread. Each of them sends a request when invoked, and the SDK now sends it from the Unity main + thread. On Android the bridge is JNI, which a thread must be attached to the JVM to enter, and a + C# worker thread is not: on Unity 2022.3 — the declared floor, measured on 2022.3.62f3 — the call + throws into the app's thread and the flow stays blocked waiting for an answer that never went + out. Unity 6 attaches the thread on demand, so it happened to work there; the hop makes it work + everywhere, and ordered with the SDK's other callbacks. iOS was unaffected. +- Requests call back on a device even when the app never sets an event listener. The platform + callback bridge was registered by the four listener setters and by nothing else, so an app that + subscribes to no events got no completion handler called at all — `Activate` included — on either + platform, and every iOS request additionally leaked the handle meant to carry its reply. The + bridge is now registered at player startup, before the first scene loads — so every call made from + the MonoBehaviour lifecycle onwards is covered. Present since 3.x: the setters are documented as + subscriptions, nothing ever said they were a precondition, and the demo app in this repository + calls all four before activating, which is why it was never hit there. +- An exception thrown by the completion handler passed to the deprecated `Adapty.GetOnboarding` now + arrives with the name of the call that raised it, and the original as `InnerException`, the way + every other Adapty call already reported one. That single method handed the exception on untouched, + so it surfaced with no indication of which callback had failed. Only `GetOnboarding` was affected. +- `AdaptyProfileParameters.SetBirthday` now sends the date the contract asks for. The key is + declared `YYYY-MM-dd` and was built by hand from the parts, without padding, so 7 March 1990 went + out as `1990-3-7` rather than `1990-03-07`. Every birthday whose month or day is below the tenth + was affected, on every platform, since 3.x. No test caught it because the only date in the + fixtures is 10 December 1815, whose month and day are both two digits. +- An offer whose branch of the contract requires an `id` is now rejected without one, at the point + it is read: promotional and win-back everywhere, and introductory on Android. The converter read + the identifier leniently for every branch, so a missing one became a null that `NullValueHandling` + then dropped from the purchase request — leaving the native side to fail the decode instead. The + error now names the missing key where it went missing. +- A subscription offer without `phases` is now rejected instead of being handed over half built. + The contract requires the key, and the converter that reads it enforced `offer_identifier` and + `type` but not this one, so a payload without it produced an offer whose phases were null — an + object that looks valid and has no prices in it. Neither native SDK can currently send such a + payload: iOS always encodes the key, and Android builds the offer only when its phases are not + empty. +- Dates reach your code as local time again, as they did in 3.x. The wire is UTC and the public API + is local — a subscription's expiry compares against `DateTime.Now` — but the payload from the + native side was being turned into a document by a reader that recognises dates while it builds the + tree, so it settled their kind before anything else had a say and every date arrived as + `DateTimeKind.Utc`. Both the event callbacks and the reply to every method were affected; only + 4.0.0-beta.1 ever behaved that way. Call `ToUniversalTime()` where you need the instant. +- A date-looking string inside an untyped payload survives as it was sent. The same reader turned + `params` of `FlowViewDidReceiveAnalyticEvent` into dates and back into strings, so + `"2026-07-30T10:00:00.000Z"` reached the listener as `"07/30/2026 10:00:00"`. +- `AdaptyPurchaseResult.ToString()` no longer throws. The contract carries `profile` in the success + branch only, and the method dereferenced it unconditionally, so describing a pending or cancelled + purchase — logging one, for instance — raised a `NullReferenceException`. - `ReportTransaction` no longer reports `DecodingFailed`. It decoded the response as a profile, while the native side returns `{"success": true}`, so the completion handler always received a decoding error even though the transaction had been reported successfully. +- **The server cluster selected through the configuration builder now reaches the native SDK.** + `ServerCluster` was the one builder field `AdaptyConfiguration`'s constructor did not copy, so + `server_cluster` was never sent and every app ran against the default cluster whatever it chose. + Selecting EU or CN did nothing in v3 and takes effect now, so an app that selected one starts + talking to that region on upgrade. The builder field is `AdaptyServerCluster?` rather than + `AdaptyServerCluster`, which is what keeps an unset cluster out of the request. +- `AdaptyPlacementFetchPolicy.Default` is no longer null. It aliases `ReloadRevalidatingCacheData` + but was declared above it, and static field initializers run in declaration order, so passing + `Default` explicitly raised a `NullReferenceException` when the request was built. +- `UpdateAttribution` sends `bool` and `DateTime` values instead of dropping them. The dictionary + serializer had branches for strings, numbers, nested dictionaries, lists and null, and none for + those two, so `UpdateAttribution(new Dictionary { { "flag", true } }, ...)` went + out as an empty object. +- `AdaptyCustomerIdentity.IsEmpty` reports an empty identity. `IosAppAccountToken` is a + non-nullable `Guid`, so comparing it to null was always false and the property never returned + true, which left the guard that keeps an empty identity out of the configuration dead. +- A partially filled date in an onboarding `date_picker` event no longer throws. The helpers reading + the optional `day`, `month` and `year` cast the nullable they received straight to `int`, which + raises `InvalidOperationException` when the key is absent — and the contract marks all three + optional. - Calling the SDK in the Editor now returns a readable "not supported on this platform" error - instead of failing to parse a null response. + instead of failing to parse a null response. That holds for the whole surface now: + `UpdateAppStoreCollectingRefundDataConsent`, `UpdateAppStoreRefundPreference` and + `PresentCodeRedemptionSheet` were guarded so that the Editor took their off-iOS branch, which + reports a null error — indistinguishable from success — so testing them in the Editor looked like + they had worked. On an Android device they still report `null`, which is unchanged. - Custom linear gradient assets are now serialized from every color and alpha key of the Unity `Gradient`. Gradients whose color and alpha keys differed in count threw `"Color keys and alpha keys arrays must have the same length"`, and gradients whose alpha keys sat @@ -57,9 +287,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `AdaptyProductIdentifier` now implements value equality, so identifiers built from a flow work as keys in the dictionary passed to `AdaptyUICreateFlowViewParameters.SetProductPurchaseParameters`; previously they were compared by - reference and the parameters silently applied to nothing. + reference and the parameters silently applied to nothing. An empty base plan id is now the same as + none, at construction, so two identifiers that always went on the wire identically are equal and + hash alike — a base plan read out of an empty text field no longer produces a key that matches + nothing. - `AdaptyFlow.VendorProductIds` and `AdaptyFlow.ProductIdentifiers` no longer return duplicates when several paywall variations of the flow offer the same product. +- `UpdateAttribution` reports an attribution graph it cannot encode through the completion handler, + as `EncodingFailed`, instead of throwing at the call site. The overload taking a dictionary is the + only public method that has to encode an argument before it can build a request, so it was the + only one whose failure escaped the transport's guard — a reference loop or a throwing getter in + the provider's data reached the caller as an exception while every other method reported an error. +- **Adapty SDK > Install Dependencies** stops on two loaded copies of Newtonsoft.Json, which is the + state the SDK's own validator already reports as an error. It examined the first copy only, and + the order loaded assemblies come back in is not specified, so the same project could be told its + dependencies were complete on one run and be sent to fix them on the next. +- **Adapty SDK > Install Dependencies** upgrades an External Dependency Manager older than the SDK + needs, instead of reporting the project complete. It checked only that a copy was loaded, so a + project coming from v3 — which declared 1.2.187 — kept it, and the iOS build resolved through a + version that gets the Xcode project path wrong for the Swift project type. Package Manager is the + only thing that can tell those versions apart: every 1.2.x build of `Google.VersionHandler` + reports the same assembly version, so a copy installed from Google's own `.unitypackage` under + `Assets/` is now reported rather than replaced — adding the package over it would leave two. +- `AdaptyConfiguration.Builder.ToString()` includes `GoogleEnablePendingPrepaidPlans`. It was the + one member missing from the description, so two builders differing only in whether Android + reports pending prepaid transactions printed identically in logs. + +### Known issues + +- **Custom color and linear gradient assets are not rendered on iOS.** The pinned AdaptySDK-iOS + 4.0.2 discards the values it receives and substitutes a transparent color and an empty gradient, + so a flow view shows neither. Nothing on the Unity side is involved: `AdaptyCustomAsset.Color` and + `AdaptyCustomAsset.LinearGradient` serialize the actual RGBA and the actual stops, and the same + substitution is in the later 4.0.3 and 4.1.0 native releases, so there is no version to move the + pin to. Custom image and video assets are unaffected. Whether Android is affected has not been + established. ## [3.17.0] - 2026-07-02 diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencies.cs b/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencies.cs new file mode 100644 index 0000000..f02a93d --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencies.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEditor.PackageManager; +using UnityEditor.PackageManager.Requests; +using UnityEngine; +using PackageInfo = UnityEditor.PackageManager.PackageInfo; + +namespace AdaptySDK.Editor +{ + /// + /// Installs the packages the SDK needs from Package Manager. A .unitypackage carries assets + /// only and cannot touch the project manifest, so nothing else can bring them in. Installs + /// whichever ones are missing, and upgrades an External Dependency Manager older than the SDK + /// needs; a copy Package Manager does not describe is reported rather than replaced. + /// + internal static class AdaptyDependencies + { + internal const string MenuPath = "Adapty SDK/Install Dependencies"; + + internal const string NewtonsoftAssembly = "Newtonsoft.Json"; + private const string EdmAssembly = "Google.VersionHandler"; + + private static AddAndRemoveRequest m_Request; + + /// + /// The Editor assembly is out of reach of the runtime resets, and with Domain Reload + /// disabled a subscription outlives entering Play Mode. + /// + /// + /// Unity passes to a callback with this attribute, so + /// the signature is not optional — a parameterless one is simply never called. + /// + /// What this run of Play Mode is reloading, if anything. + [InitializeOnEnterPlayMode] + private static void ResetInstallState(EnterPlayModeOptions options) + { + // A request in flight is an Editor operation, not something belonging to the session + // being entered: `m_Request` is also the only guard against starting a second + // `AddAndRemove` over the first, so dropping it here would let the menu item do + // exactly that. `Poll` unsubscribes itself once the request completes. + if (m_Request != null && !m_Request.IsCompleted) + { + return; + } + + EditorApplication.update -= Poll; + m_Request = null; + } + + [MenuItem(MenuPath)] + private static void Install() + { + if (m_Request != null && !m_Request.IsCompleted) + { + Debug.Log("[Adapty] Already installing, waiting for Package Manager."); + return; + } + + // Materialized once: the order of GetAssemblies() is not specified, so asking twice - + // or looking only at the first copy - decides the same project differently from run to + // run. Every state the validator calls an error has to stop the install too, or the + // menu item reports success over a project that will not compile. + var newtonsoft = Copies(NewtonsoftAssembly).ToList(); + + if (newtonsoft.Count > 1) + { + Debug.LogError(DuplicateMessage(newtonsoft, "Nothing was installed.")); + return; + } + + if (newtonsoft.Count == 1 && PackageOf(newtonsoft[0]) != AdaptyDependencyPlan.NewtonsoftId) + { + // Adding the package on top would leave two copies of it, which is its own failure. + // Nothing else is installed either: the project is in a state the user has to fix + // first, and saying anything about the other dependencies here would contradict it. + Debug.LogError(StandaloneMessage(newtonsoft[0])); + return; + } + + var edm = EdmInstalled(out var edmVersion); + var missing = AdaptyDependencyPlan.Missing(newtonsoft.Count > 0, edm, edmVersion).ToArray(); + + // Said whether or not anything is installed: a copy whose version cannot be read is + // the one case where "everything is installed" would be a guess rather than a fact. + var caution = AdaptyDependencyPlan.EdmCaution(edm, edmVersion); + if (caution != null) + { + Debug.LogWarning(caution); + } + + if (missing.Length == 0) + { + if (caution == null) + { + Debug.Log("[Adapty] Every dependency is already installed."); + } + + return; + } + + // EDM is published on OpenUPM, and a scoped registry has no public API - the project + // manifest is the only way in. + if (missing.Any(package => + package.StartsWith(AdaptyDependencyPlan.EdmId, StringComparison.Ordinal) + ) + && !EnsureRegistry()) + { + return; + } + + Debug.Log($"[Adapty] Installing {string.Join(", ", missing)}..."); + + m_Request = Client.AddAndRemove(missing, null); + EditorApplication.update += Poll; + } + + /// + /// Which copy of External Dependency Manager the project has, and the version when that is + /// something Package Manager can answer. + /// + /// + /// More than one copy counts as unmanaged: no single version answers for the project, and + /// the order GetAssemblies() returns is not specified, so picking one would decide + /// the same project differently from run to run. + /// + private static AdaptyEdmSource EdmInstalled(out string version) + { + version = null; + + var copies = Copies(EdmAssembly).ToList(); + + if (copies.Count == 0) + { + return AdaptyEdmSource.None; + } + + if (copies.Count > 1) + { + return AdaptyEdmSource.Unmanaged; + } + + var info = PackageInfo.FindForAssembly(copies[0]); + + if (info?.name != AdaptyDependencyPlan.EdmId) + { + return AdaptyEdmSource.Unmanaged; + } + + version = info.version; + return AdaptyEdmSource.Package; + } + + internal static IEnumerable Copies(string assemblyName) => + AppDomain + .CurrentDomain.GetAssemblies() + .Where(assembly => assembly.GetName().Name == assemblyName); + + internal static string PackageOf(Assembly assembly) => + PackageInfo.FindForAssembly(assembly)?.name; + + /// + /// The one wording for two copies, shared by the validator that reports the state and the + /// installer that refuses to add to it. + /// + /// Every loaded copy, listed so the user can tell them apart. + /// + /// What the caller did about it, placed before the list rather than after it - the list + /// ends in a file path, and a sentence trailing that is unreadable. + /// + internal static string DuplicateMessage(IReadOnlyList copies, string andThen = null) => + $"[Adapty] {copies.Count} copies of {NewtonsoftAssembly} are loaded, so its types are " + + "ambiguous and compilation against the Adapty SDK may fail unpredictably. " + + (andThen is null ? "" : andThen + " ") + + $"Keep one - preferably the \"{AdaptyDependencyPlan.NewtonsoftId}\" package - and remove the others:\n " + + string.Join("\n ", Describe(copies)); + + /// + /// Where each copy came from, since the name alone does not distinguish them. + /// + private static IEnumerable Describe(IEnumerable assemblies) => + assemblies.Select(assembly => + { + var name = assembly.GetName(); + string location; + try + { + location = string.IsNullOrEmpty(assembly.Location) + ? "(no file on disk)" + : assembly.Location; + } + catch (NotSupportedException) + { + location = "(location unavailable)"; + } + + return $"{name.Name} {name.Version} - {location}"; + }); + + internal static string StandaloneMessage(Assembly assembly) => + $"[Adapty] {NewtonsoftAssembly} is in this project, but not as the \"{AdaptyDependencyPlan.NewtonsoftId}\" " + + "package, and the Adapty SDK only compiles against that package. Nothing was " + + $"installed. Remove the copy at {Where(assembly)}, then run \"{MenuPath}\" again."; + + private static string Where(Assembly assembly) + { + try + { + return string.IsNullOrEmpty(assembly.Location) + ? "(no file on disk)" + : assembly.Location; + } + catch (NotSupportedException) + { + return "(location unavailable)"; + } + } + + private static void Poll() + { + if (m_Request == null || !m_Request.IsCompleted) + { + return; + } + + EditorApplication.update -= Poll; + + var request = m_Request; + m_Request = null; + + if (request.Status == StatusCode.Success) + { + Debug.Log("[Adapty] Dependencies installed."); + } + else + { + Debug.LogError($"[Adapty] Package Manager failed: {request.Error?.message}"); + } + } + + /// + /// Writes the OpenUPM registry into the project manifest. No Resolve afterwards: Package + /// Manager operations have to run one at a time, and the AddAndRemove that follows reads + /// the manifest itself. + /// + private static bool EnsureRegistry() + { + var path = Path.GetFullPath( + Path.Combine(Application.dataPath, "..", "Packages", "manifest.json") + ); + + string manifest; + try + { + manifest = File.ReadAllText(path); + } + catch (Exception error) when (error is IOException || error is UnauthorizedAccessException) + { + Debug.LogError($"[Adapty] Could not read {path}: {error.Message}"); + return false; + } + + var updated = AdaptyManifest.AddRegistry(manifest); + + if (updated == null) + { + Debug.LogError( + $"[Adapty] Could not add the \"{AdaptyManifest.RegistryName}\" registry to " + + $"{path}. Add it by hand, with \"{AdaptyManifest.RegistryScope}\" among " + + $"its scopes, and run \"{MenuPath}\" again." + ); + return false; + } + + if (updated == manifest) + { + return true; + } + + try + { + File.WriteAllText(path, updated); + } + catch (Exception error) when (error is IOException || error is UnauthorizedAccessException) + { + Debug.LogError($"[Adapty] Could not write {path}: {error.Message}"); + return false; + } + + Debug.Log( + $"[Adapty] Added the \"{AdaptyManifest.RegistryName}\" registry to the project " + + "manifest." + ); + return true; + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencies.cs.meta b/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencies.cs.meta new file mode 100644 index 0000000..3b40571 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencies.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5174e29a65a7f45ad923ca217384d176 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencyPlan.cs b/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencyPlan.cs new file mode 100644 index 0000000..4179c3b --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencyPlan.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; + +namespace AdaptySDK.Editor +{ + /// + /// Where the External Dependency Manager in this project came from, which is what decides + /// whether its version can be read at all. + /// + internal enum AdaptyEdmSource + { + /// No copy in the project. + None, + + /// The Package Manager package, whose version is exact. + Package, + + /// + /// A copy Package Manager does not describe - the one Google ships as its own + /// .unitypackage under Assets/ - or more than one copy, where no single version answers + /// for the project. + /// + Unmanaged, + } + + /// + /// What Package Manager has to install, decided from what the project already has. Kept apart + /// from the Editor code that gathers those facts, so the decision can be tested without one. + /// + internal static class AdaptyDependencyPlan + { + internal const string NewtonsoftId = "com.unity.nuget.newtonsoft-json"; + internal const string NewtonsoftVersion = "3.2.2"; + internal const string EdmId = "com.google.external-dependency-manager"; + internal const string EdmVersion = "1.2.188"; + + private static readonly Version Required = Version.Parse(EdmVersion); + + /// + /// An EDM older than the SDK needs is in the list too: to Package Manager that request is + /// an upgrade, so the one call covers both installing and moving it forward. + /// + /// Whether the Newtonsoft package is in the project. + /// Which copy of External Dependency Manager the project has. + /// + /// The version Package Manager reports for it, when it is the one describing it. + /// + internal static IEnumerable Missing( + bool newtonsoftPresent, + AdaptyEdmSource edm, + string edmVersion + ) + { + // The SDK assembly is gated on the package rather than on the assembly, so a copy that + // came from anywhere else does not make the SDK compile and is reported separately. + if (!newtonsoftPresent) + { + yield return $"{NewtonsoftId}@{NewtonsoftVersion}"; + } + + if (edm == AdaptyEdmSource.None || (edm == AdaptyEdmSource.Package && IsOlder(edmVersion))) + { + yield return $"{EdmId}@{EdmVersion}"; + } + } + + /// + /// What to say about an EDM the SDK cannot establish the version of, and null when + /// there is nothing to say. + /// + /// + /// Reported rather than installed over: adding the package on top of a copy under + /// Assets/ would leave two, which is a failure of its own. The version is not in the + /// assembly either - every 1.2.x build of Google.VersionHandler carries the same + /// 1.2.0.0, so Package Manager is the only thing that can tell 1.2.187 from 1.2.188. + /// + internal static string EdmCaution(AdaptyEdmSource edm, string edmVersion) + { + if (edm == AdaptyEdmSource.Unmanaged) + { + return "[Adapty] External Dependency Manager is in this project, but not as the " + + $"\"{EdmId}\" package, so its version cannot be read. The Adapty SDK needs " + + $"{EdmVersion} or newer: older versions get the Xcode project path wrong for " + + "the Swift project type, and the iOS build fails without naming the cause. " + + "Check the copy this project has, and update it if it is older."; + } + + if (edm == AdaptyEdmSource.Package && !Version.TryParse(edmVersion, out _)) + { + return "[Adapty] Package Manager reports External Dependency Manager as " + + $"\"{edmVersion}\", which cannot be compared as a version. The Adapty SDK " + + $"needs {EdmVersion} or newer to resolve the iOS dependencies."; + } + + return null; + } + + private static bool IsOlder(string version) => + Version.TryParse(version, out var installed) && installed < Required; + } +} diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencyPlan.cs.meta b/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencyPlan.cs.meta new file mode 100644 index 0000000..1f30b95 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyDependencyPlan.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 67661d3b65d3a417eaa5c3455875fd79 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSBuildValidator.cs b/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSBuildValidator.cs index 97ff94f..f713c7b 100644 --- a/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSBuildValidator.cs +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSBuildValidator.cs @@ -1,8 +1,3 @@ -// -// AdaptyIOSBuildValidator.cs -// AdaptySDK -// - using System; using UnityEditor; using UnityEditor.Build; diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSKidsModePostprocessor.cs b/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSKidsModePostprocessor.cs index dc7fa07..75a796a 100644 --- a/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSKidsModePostprocessor.cs +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSKidsModePostprocessor.cs @@ -1,8 +1,3 @@ -// -// AdaptyIOSKidsModePostprocessor.cs -// AdaptySDK -// - #if UNITY_IOS && ADAPTY_KIDS_MODE using System; using System.IO; @@ -17,8 +12,8 @@ namespace AdaptySDK.Editor /// /// Enables the KidsMode trait on the AdaptySDK-iOS Swift package reference in the generated /// Xcode project, so IDFA / AdSupport / AppTrackingTransparency code is compiled out of the - /// binary (App Store Kids Category / COPPA compliance). Requires an Xcode version that - /// supports Swift package traits (Xcode 26 or newer). + /// binary (App Store Kids Category / COPPA compliance). The edit itself is in + /// AdaptyIOSKidsModeTrait, which is free of Unity types so that the tests can run it. /// /// Compiled in only when the ADAPTY_KIDS_MODE scripting define is set — the same define that /// forces apple_idfa_collection_disabled in the runtime configuration. Prefer setting it in Player @@ -40,84 +35,17 @@ private static void OnPostProcessBuild(BuildTarget target, string pathToBuiltPro } var pbxPath = PBXProject.GetPBXProjectPath(pathToBuiltProject); - File.WriteAllText(pbxPath, EnableKidsModeTrait(File.ReadAllText(pbxPath))); - Debug.Log("Adapty: enabled the KidsMode trait on the AdaptySDK-iOS Swift package."); - } - internal static string EnableKidsModeTrait(string project) - { - var urlIndex = project.IndexOf("adaptyteam/AdaptySDK-iOS", StringComparison.Ordinal); - if (urlIndex < 0) + try { - throw new BuildFailedException( - "Adapty: ADAPTY_KIDS_MODE is set, but the generated Xcode project contains no " - + "AdaptySDK-iOS Swift package reference. Make sure External Dependency Manager " - + "resolved the iOS dependencies declared in AdaptySDKDependencies.xml." - ); + File.WriteAllText(pbxPath, AdaptyIOSKidsModeTrait.Enable(File.ReadAllText(pbxPath))); } - - var openIndex = project.LastIndexOf('{', urlIndex); - var closeIndex = FindMatchingBrace(project, openIndex); - var reference = project.Substring(openIndex, closeIndex - openIndex); - - if (reference.Contains("KidsMode")) - { - return project; - } - - if (reference.Contains("traits")) + catch (InvalidOperationException e) { - throw new BuildFailedException( - "Adapty: the AdaptySDK-iOS package reference already declares a traits block " - + "without KidsMode; Adapty will not merge into it automatically. Add KidsMode " - + "to that block manually, or report this so the postprocessor can be updated." - ); + throw new BuildFailedException(e.Message); } - return project.Insert(closeIndex, "\ttraits = (\n\t\t\t\tKidsMode,\n\t\t\t);\n\t\t"); - } - - private static int FindMatchingBrace(string project, int openIndex) - { - var depth = 0; - var inString = false; - for (var i = openIndex; i < project.Length; i += 1) - { - var c = project[i]; - if (inString) - { - if (c == '\\') - { - i += 1; - } - else if (c == '"') - { - inString = false; - } - continue; - } - - switch (c) - { - case '"': - inString = true; - break; - case '{': - depth += 1; - break; - case '}': - depth -= 1; - if (depth == 0) - { - return i; - } - break; - } - } - - throw new BuildFailedException( - "Adapty: failed to locate the end of the AdaptySDK-iOS package reference in project.pbxproj." - ); + Debug.Log("Adapty: enabled the KidsMode trait on the AdaptySDK-iOS Swift package."); } } } diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSKidsModeTrait.cs b/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSKidsModeTrait.cs new file mode 100644 index 0000000..a1e14c4 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSKidsModeTrait.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; + +namespace AdaptySDK.Editor +{ + /// + /// Enables the KidsMode trait on the AdaptySDK-iOS package reference in a generated + /// project.pbxproj. Free of Unity types so that the tests can run it; the build step that calls + /// it is AdaptyIOSKidsModePostprocessor. + /// + /// + /// Text surgery because nothing models this: Unity's PBXProject has no API for the traits of a + /// remote Swift package reference. Every shape this does not recognise throws, since a Kids + /// Category build that quietly keeps IDFA is worse than one that fails. + /// + internal static class AdaptyIOSKidsModeTrait + { + internal const string PackageUrl = "adaptyteam/AdaptySDK-iOS"; + internal const string Trait = "KidsMode"; + + private const string ReferenceKind = "isa = XCRemoteSwiftPackageReference"; + + /// + /// The project with the trait in place, and unchanged when it is already there. + /// + internal static string Enable(string project) + { + var (open, close) = Reference(project); + var body = project.Substring(open, close - open); + + if (body.IndexOf(Trait, StringComparison.Ordinal) >= 0) + { + return project; + } + + if (body.IndexOf("traits", StringComparison.Ordinal) >= 0) + { + throw Failed( + "the AdaptySDK-iOS package reference already declares a traits block without " + + Trait + + "; Adapty will not merge into it automatically. Add " + + Trait + + " to that block manually, or report this so the postprocessor can be updated." + ); + } + + return project.Insert(close, "\ttraits = (\n\t\t\t\t" + Trait + ",\n\t\t\t);\n\t\t"); + } + + /// + /// Where the AdaptySDK-iOS package reference begins and ends. + /// + /// + /// The object is identified by its isa and not by the brace nearest the URL alone. + /// Walking back from the URL is what picks the object, and if it ever picked the wrong one + /// the trait would be written into a neighbour — a build that reports Kids Mode and still + /// links IDFA, with nothing to notice it. Requiring the kind, and exactly one match, is + /// what makes that failure loud. + /// + private static (int Open, int Close) Reference(string project) + { + var found = new List<(int Open, int Close)>(); + + for ( + var url = project.IndexOf(PackageUrl, StringComparison.Ordinal); + url >= 0; + url = project.IndexOf(PackageUrl, url + 1, StringComparison.Ordinal) + ) + { + var open = project.LastIndexOf('{', url); + if (open < 0) + { + continue; + } + + var close = MatchingBrace(project, open); + if (close > url && project.IndexOf(ReferenceKind, open, close - open, StringComparison.Ordinal) >= 0) + { + found.Add((open, close)); + } + } + + if (found.Count == 1) + { + return found[0]; + } + + throw Failed( + found.Count == 0 + ? "the generated Xcode project contains no AdaptySDK-iOS Swift package " + + "reference. Make sure External Dependency Manager resolved the iOS " + + "dependencies declared in AdaptySDKDependencies.xml." + : "the generated Xcode project contains " + + found.Count + + " AdaptySDK-iOS Swift package references, so there is no single one to " + + "enable the trait on. Report this so the postprocessor can be updated." + ); + } + + private static int MatchingBrace(string project, int open) + { + var depth = 0; + var inString = false; + + for (var index = open; index < project.Length; index += 1) + { + var character = project[index]; + + if (inString) + { + if (character == '\\') + { + index += 1; + } + else if (character == '"') + { + inString = false; + } + continue; + } + + switch (character) + { + case '"': + inString = true; + break; + case '{': + depth += 1; + break; + case '}': + depth -= 1; + if (depth == 0) + { + return index; + } + break; + } + } + + throw Failed( + "failed to locate the end of the AdaptySDK-iOS package reference in project.pbxproj." + ); + } + + /// + /// The one exception type the build step turns into a BuildFailedException, so its own bugs + /// are not dressed up as a diagnosis of the project. + /// + internal static InvalidOperationException Failed(string message) => + new InvalidOperationException("Adapty: " + message); + } +} diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSKidsModeTrait.cs.meta b/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSKidsModeTrait.cs.meta new file mode 100644 index 0000000..fac0cf1 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyIOSKidsModeTrait.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f394db5ae2df43ab8cefd5b53d48adc1 diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyManifest.cs b/Packages/com.adapty.unity-sdk/Editor/AdaptyManifest.cs new file mode 100644 index 0000000..7cbebc1 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyManifest.cs @@ -0,0 +1,144 @@ +using System; + +namespace AdaptySDK.Editor +{ + /// + /// Edits Packages/manifest.json as text. A scoped registry has no public Package Manager API, + /// and no JSON library can be assumed present - Newtonsoft is exactly what may be missing. + /// Free of Unity types so that the tests can run it. + /// + internal static class AdaptyManifest + { + internal const string RegistryName = "package.openupm.com"; + internal const string RegistryUrl = "https://package.openupm.com"; + internal const string RegistryScope = "com.google"; + + private const string Registries = "\"scopedRegistries\""; + private const string Scopes = "\"scopes\""; + + private const string Entry = + " {\n" + + " \"name\": \"" + RegistryName + "\",\n" + + " \"url\": \"" + RegistryUrl + "\",\n" + + " \"scopes\": [\n" + + " \"" + RegistryScope + "\"\n" + + " ]\n" + + " }"; + + /// + /// Returns the manifest with the registry and the scope in place, the manifest unchanged + /// when both are already there, and null when it is shaped in a way this will not edit - + /// the caller writes nothing in that case. + /// + internal static string AddRegistry(string manifest) + { + if (string.IsNullOrEmpty(manifest)) + { + return null; + } + + var url = manifest.IndexOf(RegistryUrl, StringComparison.Ordinal); + if (url >= 0) + { + return AddScope(manifest, url); + } + + var registries = manifest.IndexOf(Registries, StringComparison.Ordinal); + return registries >= 0 ? AddEntry(manifest, registries) : AddSection(manifest); + } + + private static string AddEntry(string manifest, int registries) + { + var open = manifest.IndexOf('[', registries); + if (open < 0) + { + return null; + } + + var close = manifest.IndexOf(']', open); + if (close < 0) + { + return null; + } + + return manifest.Insert( + open + 1, + Blank(manifest, open + 1, close) ? $"\n{Entry}\n " : $"\n{Entry}," + ); + } + + private static string AddSection(string manifest) + { + var open = manifest.IndexOf('{'); + var close = manifest.LastIndexOf('}'); + if (open < 0 || close < open) + { + return null; + } + + var section = $"\n {Registries}: [\n{Entry}\n ]"; + return manifest.Insert( + open + 1, + Blank(manifest, open + 1, close) ? $"{section}\n" : $"{section}," + ); + } + + private static string AddScope(string manifest, int url) + { + var open = manifest.LastIndexOf('{', url); + var close = manifest.IndexOf('}', url); + if (open < 0 || close < 0) + { + return null; + } + + var scopes = manifest.IndexOf(Scopes, open, StringComparison.Ordinal); + if (scopes < 0 || scopes > close) + { + return null; + } + + var arrayOpen = manifest.IndexOf('[', scopes); + if (arrayOpen < 0 || arrayOpen > close) + { + return null; + } + + var arrayClose = manifest.IndexOf(']', arrayOpen); + if (arrayClose < 0) + { + return null; + } + + var body = manifest.Substring(arrayOpen + 1, arrayClose - arrayOpen - 1); + if (body.Contains($"\"{RegistryScope}\"")) + { + return manifest; + } + + return manifest.Insert( + arrayOpen + 1, + body.Trim().Length == 0 + ? $"\n \"{RegistryScope}\"\n " + : $"\n \"{RegistryScope}\"," + ); + } + + /// + /// Whether a container is empty, which decides the separating comma. Getting this wrong + /// writes a trailing comma and breaks Package Manager for the whole project. + /// + private static bool Blank(string text, int start, int end) + { + for (var index = start; index < end; index++) + { + if (!char.IsWhiteSpace(text[index])) + { + return false; + } + } + + return true; + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyManifest.cs.meta b/Packages/com.adapty.unity-sdk/Editor/AdaptyManifest.cs.meta new file mode 100644 index 0000000..52d5542 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyManifest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e76ae823f68ff460ca3c1becd3ddd8bf \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyNewtonsoftValidator.cs b/Packages/com.adapty.unity-sdk/Editor/AdaptyNewtonsoftValidator.cs new file mode 100644 index 0000000..4ba9ab2 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyNewtonsoftValidator.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace AdaptySDK.Editor +{ + /// + /// Reports every state in which Newtonsoft.Json is present but the SDK still will not build. + /// The SDK assembly is gated on the package, so a copy from anywhere else silently drops it, + /// as does no copy at all; two copies make its types ambiguous. Runs on Editor load, not at + /// build time, since none of these should reach a build. + /// + [InitializeOnLoad] + internal static class AdaptyNewtonsoftValidator + { + private const string AssemblyName = AdaptyDependencies.NewtonsoftAssembly; + private const string PackageId = AdaptyDependencyPlan.NewtonsoftId; + + static AdaptyNewtonsoftValidator() + { + var found = Loaded(); + + if (found.Count == 0) + { + Debug.LogError( + $"[Adapty] {AssemblyName} is required by the Adapty SDK but is not loaded in this " + + $"project, so the SDK is not compiled. Run \"{AdaptyDependencies.MenuPath}\" " + + $"to install the \"{PackageId}\" package." + ); + return; + } + + if (found.Count > 1) + { + Debug.LogError(AdaptyDependencies.DuplicateMessage(found)); + return; + } + + if (AdaptyDependencies.PackageOf(found[0]) != PackageId) + { + Debug.LogError(AdaptyDependencies.StandaloneMessage(found[0])); + } + } + + private static List Loaded() => + AdaptyDependencies.Copies(AssemblyName).ToList(); + } +} diff --git a/Packages/com.adapty.unity-sdk/Editor/AdaptyNewtonsoftValidator.cs.meta b/Packages/com.adapty.unity-sdk/Editor/AdaptyNewtonsoftValidator.cs.meta new file mode 100644 index 0000000..da2370a --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Editor/AdaptyNewtonsoftValidator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6dbfe064b0a7b4ac4bc17a4f6d240256 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Adapty.Events.cs b/Packages/com.adapty.unity-sdk/Runtime/Adapty.Events.cs new file mode 100644 index 0000000..2aac135 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Adapty.Events.cs @@ -0,0 +1,564 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; +using UnityEngine; +using AdaptySDK.Serialization; +using Newtonsoft.Json.Linq; +#if UNITY_IOS && !UNITY_EDITOR +using _AdaptyCallbackAction = AdaptySDK.iOS.AdaptyIOSCallbackAction; +#elif UNITY_ANDROID && !UNITY_EDITOR +using _AdaptyCallbackAction = AdaptySDK.Android.AdaptyAndroidCallbackAction; +#else +using _AdaptyCallbackAction = AdaptySDK.Noop.AdaptyNoopCallbackAction; +#endif + +namespace AdaptySDK +{ + public static partial class Adapty + { + private static IAdaptyEventListener m_Listener; + private static IAdaptyFlowsEventsListener m_FlowsEventsListener; + private static IAdaptyUISystemRequestsHandler m_SystemRequestsHandler; + private static IAdaptyUIObserverModeResolver m_ObserverModeResolver; + + // Not cleared by a reset: like the bridge registration, this is infrastructure derived from + // the environment, identical every run, and InitializeTransport re-captures it on each one. + private static SynchronizationContext m_MainThreadContext; + private static int m_MainThreadId = -1; + + /// + /// With Domain Reload disabled, statics survive leaving Play Mode, so the listeners a + /// previous run registered would still be here - and would receive the next run's events. + /// Unity calls this before the first scene of every run. + /// + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + internal static void ResetListeners() + { + m_Listener = null; + m_FlowsEventsListener = null; + m_SystemRequestsHandler = null; + m_ObserverModeResolver = null; + } + + /// + /// Registers the platform callback transport, before the first scene loads. + /// + /// + /// Nothing native reaches C# until this has run: on iOS the bridge drops a completion while + /// its delegate is null, and on Android the wrapper has no handler to post it to. It used to + /// happen inside the listener setters, which made every completion handler depend on a + /// subscription that is optional and unrelated. The stage covers the whole MonoBehaviour + /// lifecycle, and it is what binds Android's handler to the scripting thread. + /// + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + internal static void InitializeTransport() + { + // Captured beside the registration because it serves the same boundary, and the stage + // guarantees Unity's context is the current one here. + m_MainThreadContext = SynchronizationContext.Current; + m_MainThreadId = Thread.CurrentThread.ManagedThreadId; + + _AdaptyCallbackAction.InitializeOnce(); + } + + /// + /// Runs the action on the main thread: inline when the caller is already there, posted + /// through the captured otherwise. + /// + /// + /// For the requests behind the delegates the SDK hands to app code - the permission + /// respond and the observer-mode reports - which the app may invoke from any thread. + /// On Android the bridge is JNI, and a thread Unity did not attach cannot enter it, so the + /// hop is what makes "any thread" true off the main one. Without a captured context the + /// action runs inline, which is where every call was made before the capture existed. + /// + internal static void RunOnMainThread(Action action) + { + var context = m_MainThreadContext; + if (context is null || Thread.CurrentThread.ManagedThreadId == m_MainThreadId) + { + action(); + return; + } + + context.Post(_ => action(), null); + } + + /// + /// Sets the event listener for Adapty SDK events. + /// + /// The implementation to receive events, or null to detach the previous one. + public static void SetEventListener(IAdaptyEventListener listener) + { + m_Listener = listener; + } + + /// + /// Sets the event listener for flow view events. + /// + /// The implementation to receive events, or null to detach the previous one. + public static void SetFlowsEventsListener(IAdaptyFlowsEventsListener listener) + { + m_FlowsEventsListener = listener; + } + + /// + /// Sets the handler for system requests initiated by a flow (OS permission prompts and store review requests). + /// + /// The implementation to receive requests, or null to detach the previous one. + public static void SetSystemRequestsHandler(IAdaptyUISystemRequestsHandler handler) + { + m_SystemRequestsHandler = handler; + } + + /// + /// Sets the resolver for purchases and restores initiated by a flow while the SDK runs in Observer mode. + /// + /// The implementation to resolve purchases and restores, or null to detach the previous one. + public static void SetObserverModeResolver(IAdaptyUIObserverModeResolver resolver) + { + m_ObserverModeResolver = resolver; + } + + private static bool RequireEventListener(string eventId) + { + if (m_Listener == null) + { + Debug.LogWarning( + string.Format( + "[Adapty] Event listener is not set, ignoring event '{0}'. Call Adapty.SetEventListener() to receive events.", + eventId + ) + ); + return false; + } + return true; + } + + private static bool RequireFlowsListener(string eventId) + { + if (m_FlowsEventsListener == null) + { + Debug.LogWarning( + string.Format( + "[Adapty] Flows events listener is not set, ignoring event '{0}'. Call Adapty.SetFlowsEventsListener() to receive flow events.", + eventId + ) + ); + return false; + } + return true; + } + + /// + /// Entry point for every event the native side pushes. + /// + /// + /// Nothing is allowed to escape. The call arrives from native code - on iOS through a + /// reverse-P/Invoke callback with no handler behind it - so an exception here does not + /// surface as a C# error, it takes the process down on IL2CPP. A malformed payload or a + /// throwing listener is logged and the event is dropped. + /// + internal static void OnMessage(string id, string json) + { + if (string.IsNullOrEmpty(json)) + return; + + try + { + if (AdaptyJson.ParseDocument(json) is not JObject parameters) + { + return; + } + + Dispatch(id, parameters); + } + catch (Exception e) + { + Debug.LogError( + string.Format("[Adapty] Event '{0}' failed: {1}", id ?? "(null)", e) + ); + } + } + + private static T Required(JObject parameters, string key) => + AdaptyJsonRequire.Token(parameters, key).ToObject(AdaptyJson.CreateSerializerFor(typeof(T))); + + private static T Optional(JObject parameters, string key) + { + var value = parameters[key]; + return value is null || value.Type == JTokenType.Null + ? default(T) + : value.ToObject(AdaptyJson.CreateSerializerFor(typeof(T))); + } + + private static void Dispatch(string id, JObject parameters) + { + switch (id) + { + case "did_load_latest_profile": + { + if (!RequireEventListener(id)) + return; + var profile = Required(parameters, "profile"); + AdaptyCallbacks.InvokeSafe( + () => m_Listener.OnLoadLatestProfile(profile), + "Failed to invoke IAdaptyEventListener.OnLoadLatestProfile(..)" + ); + return; + } + case "on_installation_details_success": + { + if (!RequireEventListener(id)) + return; + var details = Required(parameters, "details"); + AdaptyCallbacks.InvokeSafe( + () => m_Listener.OnInstallationDetailsSuccess(details), + "Failed to invoke IAdaptyEventListener.OnInstallationDetailsSuccess(..)" + ); + return; + } + case "on_installation_details_fail": + { + if (!RequireEventListener(id)) + return; + var error = Required(parameters, "error"); + AdaptyCallbacks.InvokeSafe( + () => m_Listener.OnInstallationDetailsFail(error), + "Failed to invoke IAdaptyEventListener.OnInstallationDetailsFail(..)" + ); + return; + } + case "onboarding_did_fail_with_error": + case "onboarding_on_analytics_action": + case "onboarding_did_finish_loading": + case "onboarding_on_close_action": + case "onboarding_on_paywall_action": + case "onboarding_on_custom_action": + case "onboarding_on_state_updated_action": + OnLegacyOnboardingMessage(id, parameters); + return; + case "flow_view_did_appear": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + AdaptyCallbacks.InvokeSafe( + () => m_FlowsEventsListener.FlowViewDidAppear(view), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidAppear(..)" + ); + return; + } + case "flow_view_did_disappear": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + AdaptyCallbacks.InvokeSafe( + () => m_FlowsEventsListener.FlowViewDidDisappear(view), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidDisappear(..)" + ); + return; + } + case "flow_view_did_perform_action": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + var action = Required(parameters, "action"); + AdaptyCallbacks.InvokeSafe( + () => m_FlowsEventsListener.FlowViewDidPerformAction(view, action), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidPerformAction(..)" + ); + return; + } + case "flow_view_did_select_product": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + var productId = Required(parameters, "product_id"); + AdaptyCallbacks.InvokeSafe( + () => m_FlowsEventsListener.FlowViewDidSelectProduct(view, productId), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidSelectProduct(..)" + ); + return; + } + case "flow_view_did_start_purchase": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + var product = Required(parameters, "product"); + AdaptyCallbacks.InvokeSafe( + () => m_FlowsEventsListener.FlowViewDidStartPurchase(view, product), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidStartPurchase(..)" + ); + return; + } + case "flow_view_did_finish_purchase": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + var product = Required(parameters, "product"); + var purchaseResult = Required(parameters, "purchased_result"); + AdaptyCallbacks.InvokeSafe( + () => + m_FlowsEventsListener.FlowViewDidFinishPurchase( + view, + product, + purchaseResult + ), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFinishPurchase(..)" + ); + return; + } + case "flow_view_did_fail_purchase": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + var product = Required(parameters, "product"); + var error = Required(parameters, "error"); + AdaptyCallbacks.InvokeSafe( + () => m_FlowsEventsListener.FlowViewDidFailPurchase(view, product, error), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFailPurchase(..)" + ); + return; + } + case "flow_view_did_start_restore": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + AdaptyCallbacks.InvokeSafe( + () => m_FlowsEventsListener.FlowViewDidStartRestore(view), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidStartRestore(..)" + ); + return; + } + case "flow_view_did_finish_restore": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + var profile = Required(parameters, "profile"); + AdaptyCallbacks.InvokeSafe( + () => m_FlowsEventsListener.FlowViewDidFinishRestore(view, profile), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFinishRestore(..)" + ); + return; + } + case "flow_view_did_fail_restore": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + var error = Required(parameters, "error"); + AdaptyCallbacks.InvokeSafe( + () => m_FlowsEventsListener.FlowViewDidFailRestore(view, error), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFailRestore(..)" + ); + return; + } + case "flow_view_did_receive_error": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + var error = Required(parameters, "error"); + AdaptyCallbacks.InvokeSafe( + () => m_FlowsEventsListener.FlowViewDidReceiveError(view, error), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidReceiveError(..)" + ); + return; + } + case "flow_view_did_fail_loading_products": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + var error = Required(parameters, "error"); + AdaptyCallbacks.InvokeSafe( + () => m_FlowsEventsListener.FlowViewDidFailLoadingProducts(view, error), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFailLoadingProducts(..)" + ); + return; + } + case "flow_view_did_finish_web_payment_navigation": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + var product = Optional(parameters, "product"); + var error = Optional(parameters, "error"); + AdaptyCallbacks.InvokeSafe( + () => + m_FlowsEventsListener.FlowViewDidFinishWebPaymentNavigation( + view, + product, + error + ), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFinishWebPaymentNavigation(..)" + ); + return; + } + case "flow_view_did_receive_analytic_event": + { + if (!RequireFlowsListener(id)) + return; + var view = Required(parameters, "view"); + var name = Required(parameters, "name"); + var analyticParameters = Required>(parameters, "params"); + AdaptyCallbacks.InvokeSafe( + () => + m_FlowsEventsListener.FlowViewDidReceiveAnalyticEvent( + view, + name, + new ReadOnlyDictionary(analyticParameters) + ), + "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidReceiveAnalyticEvent(..)" + ); + return; + } + case "flow_view_did_ask_permission": + { + var view = Required(parameters, "view"); + var eventId = Required(parameters, "event_id"); + var permission = Required(parameters, "permission"); + + var customArgs = Optional>( + parameters, + "custom_args" + ); + + if (m_SystemRequestsHandler == null) + { + // Send no answer: the native HostRequestRegistry keeps the request pending + // until the view tears down, then resolves it as denied there. Fabricating an + // answer here would duplicate that fallback across two layers — this matches + // both the native no-handler behavior and the Flutter SDK. + Debug.LogWarning( + string.Format( + "[Adapty] System requests handler is not set, ignoring permission request '{0}'. Call Adapty.SetSystemRequestsHandler() to handle permission requests.", + permission + ) + ); + return; + } + + // respond(..) is typically invoked from an OS permission callback, off the main thread. + var answered = 0; + Action respond = (granted, detail) => + { + if (Interlocked.Exchange(ref answered, 1) == 1) + { + Debug.LogWarning( + "[Adapty] Permission request has already been answered, ignoring subsequent respond(..) call." + ); + return; + } + AdaptyUI.FlowViewAnswerPermission(eventId, granted, detail); + }; + + AdaptyCallbacks.InvokeSafe( + () => + m_SystemRequestsHandler.FlowViewDidAskPermission( + view, + permission, + customArgs is null + ? null + : new ReadOnlyDictionary(customArgs), + respond + ), + "Failed to invoke IAdaptyUISystemRequestsHandler.FlowViewDidAskPermission(..)" + ); + return; + } + case "flow_view_did_request_app_review": + { + var view = Required(parameters, "view"); + + if (m_SystemRequestsHandler == null) + { + AdaptyUI.RequestAppReview(null); + return; + } + + AdaptyCallbacks.InvokeSafe( + () => m_SystemRequestsHandler.FlowViewDidRequestAppReview(view), + "Failed to invoke IAdaptyUISystemRequestsHandler.FlowViewDidRequestAppReview(..)" + ); + return; + } + case "flow_view_observer_did_initiate_purchase": + { + var view = Required(parameters, "view"); + var eventId = Required(parameters, "event_id"); + var product = Required(parameters, "product"); + + if (m_ObserverModeResolver == null) + { + Debug.LogWarning( + "[Adapty] Observer mode resolver is not set, ignoring initiated purchase. Call Adapty.SetObserverModeResolver() to handle purchases in Observer mode." + ); + return; + } + + Action onStartPurchase = () => + AdaptyUI.SendObserverEvent("observer_purchase_did_start", eventId); + Action onFinishPurchase = () => + AdaptyUI.SendObserverEvent("observer_purchase_did_finish", eventId); + + AdaptyCallbacks.InvokeSafe( + () => + m_ObserverModeResolver.FlowViewDidInitiatePurchase( + view, + product, + onStartPurchase, + onFinishPurchase + ), + "Failed to invoke IAdaptyUIObserverModeResolver.FlowViewDidInitiatePurchase(..)" + ); + return; + } + case "flow_view_observer_did_initiate_restore": + { + var view = Required(parameters, "view"); + var eventId = Required(parameters, "event_id"); + + if (m_ObserverModeResolver == null) + { + Debug.LogWarning( + "[Adapty] Observer mode resolver is not set, ignoring initiated restore. Call Adapty.SetObserverModeResolver() to handle restores in Observer mode." + ); + return; + } + + Action onStartRestore = () => + AdaptyUI.SendObserverEvent("observer_restore_did_start", eventId); + Action onFinishRestore = () => + AdaptyUI.SendObserverEvent("observer_restore_did_finish", eventId); + + AdaptyCallbacks.InvokeSafe( + () => + m_ObserverModeResolver.FlowViewDidInitiateRestore( + view, + onStartRestore, + onFinishRestore + ), + "Failed to invoke IAdaptyUIObserverModeResolver.FlowViewDidInitiateRestore(..)" + ); + return; + } + default: + Debug.LogWarning( + string.Format("[Adapty] Unknown event id '{0}', ignoring.", id ?? "(null)") + ); + return; + } + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaymentMode+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Adapty.Events.cs.meta similarity index 83% rename from Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaymentMode+JSON.cs.meta rename to Packages/com.adapty.unity-sdk/Runtime/Adapty.Events.cs.meta index 706bc77..d712053 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaymentMode+JSON.cs.meta +++ b/Packages/com.adapty.unity-sdk/Runtime/Adapty.Events.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: f639d8d9c72e84307877e7ea457c7ffc +guid: 5b44ff8768ee412aa804696a154e1b40 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/com.adapty.unity-sdk/Runtime/Adapty.Overloads.cs b/Packages/com.adapty.unity-sdk/Runtime/Adapty.Overloads.cs index a46a35b..b2c0aea 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Adapty.Overloads.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Adapty.Overloads.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; +using AdaptySDK.Serialization; namespace AdaptySDK { - using AdaptySDK.SimpleJSON; - public static partial class Adapty { /// @@ -20,60 +19,6 @@ public static void GetFlowForDefaultAudience( Action completionHandler ) => GetFlowForDefaultAudience(placementId, null, completionHandler); - /// - /// This method enables you to retrieve the onboarding from the Default Audience without having to wait for the Adapty SDK to send all the user information required for segmentation to the server. - /// - /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. - /// The identifier of the onboarding localization. - /// The action that will be called with the result. - [Obsolete( - "The legacy onboarding API is deprecated in favor of Flows. Use GetFlowForDefaultAudience instead." - )] - public static void GetOnboardingForDefaultAudience( - string placementId, - string locale, - Action completionHandler - ) => GetOnboardingForDefaultAudience(placementId, locale, null, completionHandler); - - /// - /// This method enables you to retrieve the onboarding from the Default Audience without having to wait for the Adapty SDK to send all the user information required for segmentation to the server. - /// - /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. - /// By default SDK will try to load data from server and will return cached data in case of failure. Otherwise use `.returnCacheDataElseLoad` to return cached data if it exists. - /// The action that will be called with the result. - [Obsolete( - "The legacy onboarding API is deprecated in favor of Flows. Use GetFlowForDefaultAudience instead." - )] - public static void GetOnboardingForDefaultAudience( - string placementId, - AdaptyPlacementFetchPolicy fetchPolicy, - Action completionHandler - ) => GetOnboardingForDefaultAudience(placementId, null, fetchPolicy, completionHandler); - - /// - /// This method enables you to retrieve the onboarding from the Default Audience without having to wait for the Adapty SDK to send all the user information required for segmentation to the server. - /// - /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. - /// The action that will be called with the result. - [Obsolete( - "The legacy onboarding API is deprecated in favor of Flows. Use GetFlowForDefaultAudience instead." - )] - public static void GetOnboardingForDefaultAudience( - string placementId, - Action completionHandler - ) => GetOnboardingForDefaultAudience(placementId, null, null, completionHandler); - - /// - /// Adapty allows you remotely configure onboarding screens that will be displayed in your app. - /// - /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. - /// The action that will be called with the result. - [Obsolete("The legacy onboarding API is deprecated in favor of Flows. Use GetFlow instead.")] - public static void GetOnboarding( - string placementId, - Action completionHandler - ) => GetOnboarding(placementId, null, null, null, completionHandler); - /// /// Makes a purchase for the specified product. /// @@ -115,10 +60,32 @@ Action completionHandler /// The source of attribution (e.g., "appsflyer", "adjust", "branch", "custom"). /// The action that will be called with the result. public static void UpdateAttribution( - Dictionary attribution, + IReadOnlyDictionary attribution, string source, Action completionHandler - ) => UpdateAttribution(attribution.ToJSONObject().ToString(), source, completionHandler); + ) + { + // The only overload that has to encode an argument before it can build the request, + // and therefore the only one that can fail outside the transport's own guard - a + // reference loop or a throwing getter in the provider's graph. Reported the way the + // transport would have reported it rather than thrown at the caller. + string json; + try + { + json = AdaptyJson.Serialize(attribution); + } + catch (Exception exception) + { + AdaptyRequest.FailEncoding( + "update_attribution_data", + exception, + completionHandler + ); + return; + } + + UpdateAttribution(json, source, completionHandler); + } /// /// Opens the paywall in a web view or browser. @@ -157,22 +124,5 @@ public static void CreateFlowView( Action completionHandler ) => CreateFlowView(flow, null, completionHandler); - /// - /// Creates an onboarding view from an AdaptyOnboarding object. - /// - /// An object for which you are trying to create a view. - /// The action that will be called with the result. The result contains an object. - [Obsolete( - "The legacy onboarding API is deprecated in favor of Flows. Use CreateFlowView instead." - )] - public static void CreateOnboardingView( - AdaptyOnboarding onboarding, - Action completionHandler - ) => - CreateOnboardingView( - onboarding, - AdaptyWebPresentation.ExternalBrowser, - completionHandler - ); } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Adapty.cs b/Packages/com.adapty.unity-sdk/Runtime/Adapty.cs index 7a0e040..cc2d632 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Adapty.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Adapty.cs @@ -1,17 +1,11 @@ using System; using System.Collections.Generic; -#if UNITY_IOS && !UNITY_EDITOR -using _Adapty = AdaptySDK.iOS.AdaptyIOS; -#elif UNITY_ANDROID && !UNITY_EDITOR -using _Adapty = AdaptySDK.Android.AdaptyAndroid; -#else -using _Adapty = AdaptySDK.Noop.AdaptyNoop; -#endif +using System.Collections.ObjectModel; +using AdaptySDK.Serialization; +using Newtonsoft.Json.Linq; namespace AdaptySDK { - using AdaptySDK.SimpleJSON; - /// /// The main class for interacting with the Adapty SDK. /// @@ -20,7 +14,7 @@ public static partial class Adapty /// /// The version of the Adapty SDK. /// - public static readonly string SDKVersion = "4.0.0"; + public static readonly string SDKVersion = "4.0.0-beta.2"; /// /// Use this method to initialize the Adapty SDK. @@ -42,28 +36,10 @@ public static void Activate( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("configuration", configuration.ToJSONNode()); + var parameters = new JObject(); + parameters["configuration"] = AdaptyJson.ToNode(configuration); - Request.Send( - "activate", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.Activate(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("activate", parameters, completionHandler); } /// @@ -98,96 +74,27 @@ public static void GetFlow( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("placement_id", placementId); - - if (fetchPolicy != null) - { - parameters.Add("fetch_policy", fetchPolicy.ToJSONNode()); - } - - if (loadTimeout.HasValue) - { - parameters.Add("load_timeout", loadTimeout.Value.TotalSeconds); - } - - Request.Send( - "get_flow", - parameters, - JSONNodeExtensions.GetFlow, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.GetFlow(..)", - e - ); - } - } - ); - } - - /// - /// Adapty allows you remotely configure onboarding screens that will be displayed in your app. - /// This way you don't have to hardcode the onboarding content and can dynamically change it or run A/B tests without app releases. - /// - /// - /// Read more at Adapty Documentation - /// - /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. - /// The identifier of the onboarding localization. - /// By default SDK will try to load data from server and will return cached data in case of failure. Otherwise use `.returnCacheDataElseLoad` to return cached data if it exists. - /// The timeout for the onboarding loading. - /// The action that will be called with the result. - [Obsolete("The legacy onboarding API is deprecated in favor of Flows. Use GetFlow instead.")] - public static void GetOnboarding( - string placementId, - string locale, - AdaptyPlacementFetchPolicy fetchPolicy, - TimeSpan? loadTimeout, - Action completionHandler - ) - { - var parameters = new JSONObject(); - - parameters.Add("placement_id", placementId); - - if (locale != null) - { - parameters.Add("locale", locale); - } + var parameters = new JObject(); + parameters["placement_id"] = placementId; if (fetchPolicy != null) { - parameters.Add("fetch_policy", fetchPolicy.ToJSONNode()); + parameters["fetch_policy"] = AdaptyJson.ToNode(fetchPolicy); } if (loadTimeout.HasValue) { - parameters.Add("load_timeout", loadTimeout.Value.TotalSeconds); + parameters["load_timeout"] = loadTimeout.Value.TotalSeconds; } - Request.Send( - "get_onboarding", - parameters, - JSONNodeExtensions.GetOnboarding, - (value, error) => - { - completionHandler?.Invoke(value, error); - } - ); + AdaptyRequest.Send("get_flow", parameters, completionHandler); } /// /// This method enables you to retrieve the flow from the Default Audience without having to wait for the Adapty SDK to send all the user information required for segmentation to the server. /// /// - /// Read more at Adapty Documentation + /// Read more at Adapty Documentation /// /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. /// By default SDK will try to load data from server and will return cached data in case of failure. Otherwise use `.returnCacheDataElseLoad` to return cached data if it exists. @@ -198,87 +105,15 @@ public static void GetFlowForDefaultAudience( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("placement_id", placementId); - - if (fetchPolicy != null) - { - parameters.Add("fetch_policy", fetchPolicy.ToJSONNode()); - } - - Request.Send( - "get_flow_for_default_audience", - parameters, - JSONNodeExtensions.GetFlow, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.GetFlowForDefaultAudience(..)", - e - ); - } - } - ); - } - - /// - /// This method enables you to retrieve the onboarding from the Default Audience without having to wait for the Adapty SDK to send all the user information required for segmentation to the server. - /// - /// - /// Read more at Adapty Documentation - /// - /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. - /// The identifier of the onboarding localization. - /// By default SDK will try to load data from server and will return cached data in case of failure. Otherwise use `.returnCacheDataElseLoad` to return cached data if it exists. - /// The action that will be called with the result. - [Obsolete( - "The legacy onboarding API is deprecated in favor of Flows. Use GetFlowForDefaultAudience instead." - )] - public static void GetOnboardingForDefaultAudience( - string placementId, - string locale, - AdaptyPlacementFetchPolicy fetchPolicy, - Action completionHandler - ) - { - var parameters = new JSONObject(); - parameters.Add("placement_id", placementId); - - if (locale != null) - { - parameters.Add("locale", locale); - } + var parameters = new JObject(); + parameters["placement_id"] = placementId; if (fetchPolicy != null) { - parameters.Add("fetch_policy", fetchPolicy.ToJSONNode()); + parameters["fetch_policy"] = AdaptyJson.ToNode(fetchPolicy); } - Request.Send( - "get_onboarding_for_default_audience", - parameters, - JSONNodeExtensions.GetOnboarding, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.GetOnboardingForDefaultAudience(..)", - e - ); - } - } - ); + AdaptyRequest.Send("get_flow_for_default_audience", parameters, completionHandler); } /// @@ -292,30 +127,20 @@ Action completionHandler /// The action that will be called with the result. The result contains a list of objects. public static void GetPaywallProducts( AdaptyFlow flow, - Action, AdaptyError> completionHandler + Action, AdaptyError> completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("flow", flow.ToJSONNode()); + var parameters = new JObject(); + parameters["flow"] = AdaptyJson.ToNode(flow); - Request.Send( + AdaptyRequest.Send>( "get_paywall_products", parameters, - JSONNodeExtensions.GetAdaptyPaywallProductList, (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action,AdaptyError> completionHandler in Adapty.GetPaywallProducts(..)", - e - ); - } - } + completionHandler?.Invoke( + value is null ? null : new ReadOnlyCollection(value), + error + ) ); } @@ -331,25 +156,7 @@ Action, AdaptyError> completionHandler /// The action that will be called with the result. The result contains an object. public static void GetProfile(Action completionHandler) { - Request.Send( - "get_profile", - null, - JSONNodeExtensions.GetAdaptyProfile, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.GetProfile(..)", - e - ); - } - } - ); + AdaptyRequest.Send("get_profile", null, completionHandler); } /// @@ -387,9 +194,9 @@ public static void Identify( Action completionHandler ) { - var parameters = new JSONObject(); + var parameters = new JObject(); - parameters.Add("customer_user_id", customerUserId); + parameters["customer_user_id"] = customerUserId; var customerIdentity = new AdaptyCustomerIdentity( iosAppAccountToken, @@ -398,28 +205,10 @@ Action completionHandler if (!customerIdentity.IsEmpty) { - parameters.Add("parameters", customerIdentity.ToJSONNode()); + parameters["parameters"] = AdaptyJson.ToNode(customerIdentity); } - Request.Send( - "identify", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.Identify(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("identify", parameters, completionHandler); } /// @@ -428,52 +217,16 @@ Action completionHandler /// The action that will be called with the result. The result contains a boolean value indicating whether the SDK is activated. public static void IsActivated(Action completionHandler) { - Request.Send( - "is_activated", - null, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.IsActivated(..)", - e - ); - } - } - ); + AdaptyRequest.Send("is_activated", null, completionHandler); } /// /// Returns the current log level of the Adapty SDK. /// /// The action that will be called with the result. The result contains the current value. - public static void GetLoglevel(Action completionHandler) + public static void GetLogLevel(Action completionHandler) { - Request.Send( - "get_log_level", - null, - JSONNodeExtensions.GetAdaptyLogLevel, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.GetLoglevel(..)", - e - ); - } - } - ); + AdaptyRequest.Send("get_log_level", null, completionHandler); } /// @@ -486,28 +239,10 @@ public static void GetLoglevel(Action completionHan /// The action that will be called with the result. public static void SetLogLevel(AdaptyLogLevel level, Action completionHandler) { - var parameters = new JSONObject(); - parameters.Add("value", level.ToJSONNode()); + var parameters = new JObject(); + parameters["value"] = AdaptyJson.ToNode(level); - Request.Send( - "set_log_level", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.SetLogLevel(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("set_log_level", parameters, completionHandler); } /// @@ -522,25 +257,7 @@ public static void GetCurrentInstallationStatus( Action completionHandler ) { - Request.Send( - "get_current_installation_status", - null, - JSONNodeExtensions.GetInstallationStatus, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.GetCurrentInstallationStatus(..)", - e - ); - } - } - ); + AdaptyRequest.Send("get_current_installation_status", null, completionHandler); } /// @@ -553,25 +270,7 @@ Action completionHandler /// The action that will be called with the result. public static void Logout(Action completionHandler) { - Request.Send( - "logout", - null, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.Logout(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("logout", null, completionHandler); } /// @@ -588,28 +287,10 @@ public static void CreateWebPaywallUrl( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("paywall", paywall.ToJSONNode()); + var parameters = new JObject(); + parameters["paywall"] = AdaptyJson.ToNode(paywall); - Request.Send( - "create_web_paywall_url", - parameters, - JSONNodeExtensions.GetString, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.CreateWebPaywallUrl(..)", - e - ); - } - } - ); + AdaptyRequest.Send("create_web_paywall_url", parameters, completionHandler); } /// @@ -626,28 +307,10 @@ public static void CreateWebPaywallUrl( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("product", product.ToJSONNode()); + var parameters = new JObject(); + parameters["product"] = AdaptyJson.ToNode(new AdaptyPaywallProductRequest(product)); - Request.Send( - "create_web_paywall_url", - parameters, - JSONNodeExtensions.GetString, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.CreateWebPaywallUrl(..)", - e - ); - } - } - ); + AdaptyRequest.Send("create_web_paywall_url", parameters, completionHandler); } /// @@ -666,29 +329,11 @@ public static void OpenWebPaywall( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("paywall", paywall.ToJSONNode()); - parameters.Add("open_in", openIn.ToJSONNode()); + var parameters = new JObject(); + parameters["paywall"] = AdaptyJson.ToNode(paywall); + parameters["open_in"] = AdaptyJson.ToNode(openIn); - Request.Send( - "open_web_paywall", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.OpenWebPaywall(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("open_web_paywall", parameters, completionHandler); } /// @@ -707,29 +352,11 @@ public static void OpenWebPaywall( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("product", product.ToJSONNode()); - parameters.Add("open_in", openIn.ToJSONNode()); + var parameters = new JObject(); + parameters["product"] = AdaptyJson.ToNode(new AdaptyPaywallProductRequest(product)); + parameters["open_in"] = AdaptyJson.ToNode(openIn); - Request.Send( - "open_web_paywall", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.OpenWebPaywall(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("open_web_paywall", parameters, completionHandler); } /// @@ -745,28 +372,10 @@ Action completionHandler /// The action that will be called with the result. public static void LogShowFlow(AdaptyFlow flow, Action completionHandler) { - var parameters = new JSONObject(); - parameters.Add("flow", flow.ToJSONNode()); + var parameters = new JObject(); + parameters["flow"] = AdaptyJson.ToNode(flow); - Request.Send( - "log_show_flow", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.LogShowFlow(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("log_show_flow", parameters, completionHandler); } /// @@ -783,41 +392,16 @@ public static void UpdateAppStoreCollectingRefundDataConsent( Action completionHandler ) { -#if UNITY_IOS && !UNITY_EDITOR - var parameters = new JSONObject(); - parameters.Add("consent", consent); +#if UNITY_IOS || UNITY_EDITOR + var parameters = new JObject(); + parameters["consent"] = consent; - Request.Send( - "update_collecting_refund_data_consent", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.UpdateAppStoreCollectingRefundDataConsent(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("update_collecting_refund_data_consent", parameters, completionHandler); #else - try - { - completionHandler?.Invoke(null); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.UpdateAppStoreCollectingRefundDataConsent(..)", - e - ); - } + AdaptyCallbacks.InvokeSafe( + () => completionHandler?.Invoke(null), + $"Failed to invoke completionHandler in {nameof(UpdateAppStoreCollectingRefundDataConsent)}(..)" + ); #endif } @@ -826,7 +410,7 @@ Action completionHandler /// /// /// This method is iOS-only and allows you to set how refunds should be handled for a specific user. - /// Read more on the Adapty Documentation + /// Read more on the Adapty Documentation /// /// The value to set. /// The action that will be called with the result. @@ -835,41 +419,16 @@ public static void UpdateAppStoreRefundPreference( Action completionHandler ) { -#if UNITY_IOS && !UNITY_EDITOR - var parameters = new JSONObject(); - parameters.Add("refund_preference", refundPreference.ToJSONNode()); +#if UNITY_IOS || UNITY_EDITOR + var parameters = new JObject(); + parameters["refund_preference"] = AdaptyJson.ToNode(refundPreference); - Request.Send( - "update_refund_preference", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.UpdateAppStoreRefundPreference(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("update_refund_preference", parameters, completionHandler); #else - try - { - completionHandler?.Invoke(null); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.UpdateAppStoreRefundPreference(..)", - e - ); - } + AdaptyCallbacks.InvokeSafe( + () => completionHandler?.Invoke(null), + $"Failed to invoke completionHandler in {nameof(UpdateAppStoreRefundPreference)}(..)" + ); #endif } @@ -889,32 +448,14 @@ public static void MakePurchase( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("product", product.ToJSONNode()); + var parameters = new JObject(); + parameters["product"] = AdaptyJson.ToNode(new AdaptyPaywallProductRequest(product)); if (purchaseParameters != null) { - parameters.Add("parameters", purchaseParameters.ToJSONNode()); + parameters["parameters"] = AdaptyJson.ToNode(purchaseParameters); } - Request.Send( - "make_purchase", - parameters, - JSONNodeExtensions.GetAdaptyPurchaseResult, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.MakePurchase(..)", - e - ); - } - } - ); + AdaptyRequest.Send("make_purchase", parameters, completionHandler); } /// @@ -927,38 +468,13 @@ Action completionHandler /// The action that will be called with the result. public static void PresentCodeRedemptionSheet(Action completionHandler) { -#if UNITY_IOS && !UNITY_EDITOR - Request.Send( - "present_code_redemption_sheet", - null, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.PresentCodeRedemptionSheet(..)", - e - ); - } - } - ); +#if UNITY_IOS || UNITY_EDITOR + AdaptyRequest.SendVoid("present_code_redemption_sheet", null, completionHandler); #else - try - { - completionHandler?.Invoke(null); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.PresentCodeRedemptionSheet(..)", - e - ); - } + AdaptyCallbacks.InvokeSafe( + () => completionHandler?.Invoke(null), + $"Failed to invoke completionHandler in {nameof(PresentCodeRedemptionSheet)}(..)" + ); #endif } @@ -980,32 +496,14 @@ public static void ReportTransaction( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("transaction_id", transactionId); + var parameters = new JObject(); + parameters["transaction_id"] = transactionId; if (variationId != null) { - parameters.Add("variation_id", variationId); + parameters["variation_id"] = variationId; } - Request.Send( - "report_transaction", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.ReportTransaction(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("report_transaction", parameters, completionHandler); } /// @@ -1019,25 +517,7 @@ Action completionHandler /// The action that will be called with the result. The result contains an object. public static void RestorePurchases(Action completionHandler) { - Request.Send( - "restore_purchases", - null, - JSONNodeExtensions.GetAdaptyProfile, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.RestorePurchases(..)", - e - ); - } - } - ); + AdaptyRequest.Send("restore_purchases", null, completionHandler); } /// @@ -1049,25 +529,7 @@ public static void RestorePurchases(Action completio /// The action that will be called with the result. The result contains the native SDK version string. public static void GetNativeSDKVersion(Action completionHandler) { - Request.Send( - "get_sdk_version", - null, - JSONNodeExtensions.GetString, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.GetNativeSDKVersion(..)", - e - ); - } - } - ); + AdaptyRequest.Send("get_sdk_version", null, completionHandler); } /// @@ -1083,36 +545,15 @@ public static void GetNativeSDKVersion(Action completionHan /// The action that will be called with the result. public static void SetFallback(string fileName, Action completionHandler) { - var parameters = new JSONObject(); + var parameters = new JObject(); #if UNITY_IOS && !UNITY_EDITOR - parameters.Add("path", UnityEngine.Application.dataPath + "/Raw/" + fileName); + parameters["path"] = UnityEngine.Application.dataPath + "/Raw/" + fileName; #elif UNITY_ANDROID && !UNITY_EDITOR - parameters.Add( - "path", - "jar:file://" + UnityEngine.Application.dataPath + "!/assets/" + fileName - ); + parameters["path"] = "jar:file://" + UnityEngine.Application.dataPath + "!/assets/" + fileName; #endif - Request.Send( - "set_fallback", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.SetFallback(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("set_fallback", parameters, completionHandler); } /// @@ -1130,30 +571,11 @@ public static void SetIntegrationIdentifier( Action completionHandler ) { - var parameters = new JSONObject(); - var identifier = new JSONObject(); - identifier.Add(key, value); - parameters.Add("key_values", identifier); + var parameters = new JObject(); + var identifier = new JObject { [key] = value }; + parameters["key_values"] = identifier; - Request.Send( - "set_integration_identifiers", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.SetIntegrationIdentifier(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("set_integration_identifiers", parameters, completionHandler); } /// @@ -1172,29 +594,11 @@ public static void UpdateAttribution( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("attribution", jsonString); - parameters.Add("source", source); + var parameters = new JObject(); + parameters["attribution"] = jsonString; + parameters["source"] = source; - Request.Send( - "update_attribution_data", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.UpdateAttribution(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("update_attribution_data", parameters, completionHandler); } /// @@ -1212,31 +616,16 @@ public static void UpdateProfile( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("params", param.ToJSONNode()); + var parameters = new JObject(); + parameters["params"] = AdaptyJson.ToNode(param); - Request.Send( - "update_profile", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.UpdateProfile(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("update_profile", parameters, completionHandler); } } + /// + /// Building, presenting and dismissing the views that render a flow. + /// public static partial class AdaptyUI { /// @@ -1255,143 +644,20 @@ public static void CreateFlowView( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("flow", flow.ToJSONNode()); + var parameters = new JObject(); + parameters["flow"] = AdaptyJson.ToNode(flow); if (optionalParameters != null) { - if (optionalParameters.Locale != null) - { - parameters.Add("locale", optionalParameters.Locale); - } - - if (optionalParameters.LoadTimeout.HasValue) - { - parameters.Add( - "load_timeout", - optionalParameters.LoadTimeout.Value.TotalSeconds - ); - } - - if (optionalParameters.PreloadProducts.HasValue) + // The optional parameters are contract members of the same request object, not a + // nested one, so the serialized form is merged in rather than added under a key. + foreach (var entry in (JObject)AdaptyJson.ToNode(optionalParameters)) { - parameters.Add("preload_products", optionalParameters.PreloadProducts.Value); - } - - if (optionalParameters.CustomTags != null) - { - var node = new JSONObject(); - foreach (KeyValuePair entry in optionalParameters.CustomTags) - { - node.Add(entry.Key, entry.Value); - } - parameters.Add("custom_tags", node); - } - if (optionalParameters.CustomTimers != null) - { - var node = new JSONObject(); - foreach ( - KeyValuePair entry in optionalParameters.CustomTimers - ) - { - node.Add(entry.Key, entry.Value.ToJSONNode()); - } - parameters.Add("custom_timers", node); - } - if (optionalParameters.ProductPurchaseParameters != null) - { - var parametersNode = new JSONObject(); - - foreach ( - KeyValuePair< - AdaptyProductIdentifier, - AdaptyPurchaseParameters - > entry in optionalParameters.ProductPurchaseParameters - ) - { - parametersNode.Add(entry.Key._AdaptyProductId, entry.Value.ToJSONNode()); - } - - parameters.Add("product_purchase_parameters", parametersNode); - } - - if (optionalParameters.CustomAssets != null) - { - parameters.Add("custom_assets", optionalParameters.CustomAssets.ToJSONNode()); - } - - if (optionalParameters.EnableSafeAreaPaddings.HasValue) - { - parameters.Add( - "enable_safe_area_paddings", - optionalParameters.EnableSafeAreaPaddings.Value - ); + parameters[entry.Key] = entry.Value; } } - Request.Send( - "adapty_ui_create_flow_view", - parameters, - JSONNodeExtensions.GetAdaptyUIFlowView, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.CreateFlowView(..)", - e - ); - } - } - ); - } - - /// - /// Creates an onboarding view from an AdaptyOnboarding object. - /// - /// - /// Right after receiving an , you can create the corresponding to present it afterwards. - /// Read more at Adapty Documentation - /// - /// An object for which you are trying to create a view. - /// Controls how external URLs are presented in the onboarding (in-app browser vs external browser). Default is . - /// The action that will be called with the result. The result contains an object. - [Obsolete( - "The legacy onboarding API is deprecated in favor of Flows. Use CreateFlowView instead." - )] - public static void CreateOnboardingView( - AdaptyOnboarding onboarding, - AdaptyWebPresentation externalUrlsPresentation, - Action completionHandler - ) - { - var parameters = new JSONObject(); - parameters.Add("onboarding", onboarding.ToJSONNode()); - parameters.Add("external_urls_presentation", externalUrlsPresentation.ToJSONNode()); - - Request.Send( - "adapty_ui_create_onboarding_view", - parameters, - JSONNodeExtensions.GetAdaptyUIOnboardingView, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.CreateOnboardingView(..)", - e - ); - } - } - ); + AdaptyRequest.Send("adapty_ui_create_flow_view", parameters, completionHandler); } /// @@ -1408,29 +674,11 @@ public static void DismissFlowView( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("id", view.Id); - parameters.Add("destroy", true); + var parameters = new JObject(); + parameters["id"] = view.Id; + parameters["destroy"] = true; - Request.Send( - "adapty_ui_dismiss_flow_view", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.DismissFlowView(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("adapty_ui_dismiss_flow_view", parameters, completionHandler); } /// @@ -1458,138 +706,11 @@ public static void PresentFlowView( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("id", view.Id); - parameters.Add("ios_presentation_style", iosPresentationStyle.ToJSONNode()); - - Request.Send( - "adapty_ui_present_flow_view", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.PresentFlowView(..)", - e - ); - } - } - ); - } - - /// - /// Presents the onboarding view to the user. - /// - /// - /// This method presents the onboarding view using the default full-screen presentation style. - /// - /// An object representing the view to present. - /// The action that will be called with the result. - [Obsolete( - "The legacy onboarding API is deprecated in favor of Flows. Use PresentFlowView instead." - )] - public static void PresentOnboardingView( - AdaptyUIOnboardingView view, - Action completionHandler - ) - { - PresentOnboardingView(view, AdaptyUIIOSPresentationStyle.FullScreen, completionHandler); - } - - /// - /// Presents the onboarding view to the user with a specified presentation style. - /// - /// - /// This method presents the onboarding view using the specified iOS presentation style (iOS only). - /// - /// An object representing the view to present. - /// An object representing the iOS presentation style (iOS only). - /// The action that will be called with the result. - [Obsolete( - "The legacy onboarding API is deprecated in favor of Flows. Use PresentFlowView instead." - )] - public static void PresentOnboardingView( - AdaptyUIOnboardingView view, - AdaptyUIIOSPresentationStyle iosPresentationStyle, - Action completionHandler - ) - { - var parameters = new JSONObject(); - parameters.Add("id", view.Id); - parameters.Add("ios_presentation_style", iosPresentationStyle.ToJSONNode()); - - Request.Send( - "adapty_ui_present_onboarding_view", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.PresentOnboardingView(..)", - e - ); - } - } - ); - } - - /// - /// Dismisses the onboarding view. - /// - /// - /// Call this method when you want to dismiss the onboarding view from the screen. - /// - /// An object representing the view to dismiss. - /// The action that will be called with the result. - [Obsolete( - "The legacy onboarding API is deprecated in favor of Flows. Use DismissFlowView instead." - )] - public static void DismissOnboardingView( - AdaptyUIOnboardingView view, - Action completionHandler - ) => DismissOnboardingView(view, false, completionHandler); - - private static void DismissOnboardingView( - AdaptyUIOnboardingView view, - bool destroy, - Action completionHandler - ) - { - var parameters = new JSONObject(); - parameters.Add("id", view.Id); - parameters.Add("destroy", destroy); + var parameters = new JObject(); + parameters["id"] = view.Id; + parameters["ios_presentation_style"] = AdaptyJson.ToNode(iosPresentationStyle); - Request.Send( - "adapty_ui_dismiss_onboarding_view", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.DismissOnboardingView(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("adapty_ui_present_flow_view", parameters, completionHandler); } /// @@ -1610,53 +731,17 @@ Action completionHandler ShowDialog(view.Id, configuration, completionHandler); } - /// - /// Presents a dialog on the onboarding view. - /// - /// - /// This method shows a dialog with custom configuration on the onboarding view. The dialog can be used for various purposes like showing terms, privacy policy, or custom messages. - /// - /// An object representing the view on which to show the dialog. - /// An object that contains the dialog configuration. - /// The action that will be called with the result. The result contains the indicating which action was taken. - public static void ShowDialog( - AdaptyUIOnboardingView view, - AdaptyUIDialogConfiguration configuration, - Action completionHandler - ) - { - ShowDialog(view.Id, configuration, completionHandler); - } - private static void ShowDialog( string viewId, AdaptyUIDialogConfiguration configuration, Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("id", viewId); - parameters.Add("configuration", configuration.ToJSONNode()); + var parameters = new JObject(); + parameters["id"] = viewId; + parameters["configuration"] = AdaptyJson.ToNode(configuration); - Request.Send( - "adapty_ui_show_dialog", - parameters, - JSONNodeExtensions.GetAdaptyUIDialogActionType, - (value, error) => - { - try - { - completionHandler?.Invoke(value, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in Adapty.ShowDialog(..)", - e - ); - } - } - ); + AdaptyRequest.Send("adapty_ui_show_dialog", parameters, completionHandler); } /// @@ -1674,29 +759,11 @@ public static void OpenUrl( Action completionHandler ) { - var parameters = new JSONObject(); - parameters.Add("url", url); - parameters.Add("open_in", openIn.ToJSONNode()); + var parameters = new JObject(); + parameters["url"] = url; + parameters["open_in"] = AdaptyJson.ToNode(openIn); - Request.Send( - "adapty_ui_open_url", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in AdaptyUI.OpenUrl(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("adapty_ui_open_url", parameters, completionHandler); } /// @@ -1708,56 +775,41 @@ Action completionHandler /// The action that will be called with the result. public static void RequestAppReview(Action completionHandler) { - Request.Send( - "adapty_ui_request_app_review", - null, - JSONNodeExtensions.GetBoolean, - (value, error) => - { - try - { - completionHandler?.Invoke(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke Action completionHandler in AdaptyUI.RequestAppReview(..)", - e - ); - } - } - ); + AdaptyRequest.SendVoid("adapty_ui_request_app_review", null, completionHandler); } + // The two senders behind the delegates the SDK hands to app code, which the app may invoke + // from any thread - hence the hop, see Adapty.RunOnMainThread. + internal static void FlowViewAnswerPermission(string eventId, bool granted, string detail) { - var parameters = new JSONObject(); - parameters.Add("event_id", eventId); - parameters.Add("status", granted ? "granted" : "denied"); - if (detail != null) + Adapty.RunOnMainThread(() => { - parameters.Add("detail", detail); - } + var parameters = new JObject(); + parameters["event_id"] = eventId; + parameters["status"] = granted ? "granted" : "denied"; + if (detail != null) + { + parameters["detail"] = detail; + } - Request.Send( - "flow_view_did_answer_permission", - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => LogRoundTripError("flow_view_did_answer_permission", error) - ); + AdaptyRequest.SendVoid( + "flow_view_did_answer_permission", + parameters, + (error) => LogRoundTripError("flow_view_did_answer_permission", error) + ); + }); } internal static void SendObserverEvent(string method, string eventId) { - var parameters = new JSONObject(); - parameters.Add("event_id", eventId); + Adapty.RunOnMainThread(() => + { + var parameters = new JObject(); + parameters["event_id"] = eventId; - Request.Send( - method, - parameters, - JSONNodeExtensions.GetBoolean, - (value, error) => LogRoundTripError(method, error) - ); + AdaptyRequest.SendVoid(method, parameters, (error) => LogRoundTripError(method, error)); + }); } private static void LogRoundTripError(string method, AdaptyError error) @@ -1776,46 +828,4 @@ private static void LogRoundTripError(string method, AdaptyError error) ); } } - - internal static class Request - { - internal static void Send( - string method, - JSONObject request, - Func mapResponseValue, - Action completionHandler - ) - { - string stringJson; - try - { - if (request == null) - { - request = new JSONObject(); - } - request.Add("method", method); - stringJson = request.ToString(); - } - catch (Exception ex) - { - var error = new AdaptyError( - AdaptyErrorCode.EncodingFailed, - $"Failed encoding request: {method}", - $"AdaptyUnityError.EncodingFailed({ex})" - ); - completionHandler(default(T), error); - return; - } - - _Adapty.Invoke( - method, - stringJson, - (json) => - { - var result = json.GetAdaptyResult(mapResponseValue); - completionHandler(result.Value, result.Error); - } - ); - } - } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/AdaptyCallbacks.cs b/Packages/com.adapty.unity-sdk/Runtime/AdaptyCallbacks.cs new file mode 100644 index 0000000..3d8935a --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/AdaptyCallbacks.cs @@ -0,0 +1,30 @@ +using System; + +namespace AdaptySDK +{ + /// + /// The one policy for calling back into the app, and the only implementation of it. + /// + /// + /// Safe does not mean swallowed. The app's own exception is rethrown with the context of the + /// call that raised it and the original as , which is + /// what a caller sees on a request and what Adapty.OnMessage logs on an event - that + /// boundary is a reverse P/Invoke with no handler behind it, and it keeps its own guard. + /// Requests reach this through AdaptyRequest, which supplies the wording; events name + /// themselves at the call site, since the listener method is not the enclosing one. + /// + internal static class AdaptyCallbacks + { + internal static void InvokeSafe(Action invocation, string failureContext) + { + try + { + invocation(); + } + catch (Exception e) + { + throw new Exception(failureContext, e); + } + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyError+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/AdaptyCallbacks.cs.meta similarity index 83% rename from Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyError+JSON.cs.meta rename to Packages/com.adapty.unity-sdk/Runtime/AdaptyCallbacks.cs.meta index fa06a14..29ba166 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyError+JSON.cs.meta +++ b/Packages/com.adapty.unity-sdk/Runtime/AdaptyCallbacks.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: a5e7bb8354f2e4f47a49debfbb628e47 +guid: fafa356012f84c078efc07b108a2ac50 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/com.adapty.unity-sdk/Runtime/AdaptyRequest.cs b/Packages/com.adapty.unity-sdk/Runtime/AdaptyRequest.cs new file mode 100644 index 0000000..4d31438 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/AdaptyRequest.cs @@ -0,0 +1,148 @@ +using System; +using System.Runtime.CompilerServices; +#if UNITY_IOS && !UNITY_EDITOR +using _Adapty = AdaptySDK.iOS.AdaptyIOS; +#elif UNITY_ANDROID && !UNITY_EDITOR +using _Adapty = AdaptySDK.Android.AdaptyAndroid; +#else +using _Adapty = AdaptySDK.Noop.AdaptyNoop; +#endif +using AdaptySDK.Serialization; +using Newtonsoft.Json.Linq; + +namespace AdaptySDK +{ + /// + /// The one way a public method reaches the native side, and the one place a request names + /// itself when the app's completion handler throws. + /// + /// + /// and are the only entry points: the raw + /// transport is private, so a call site cannot reach the bridge without the guard, and neither + /// can spell the diagnostic wrong - [CallerMemberName] is what fills the name in. + /// + internal static class AdaptyRequest + { + /// + /// Sends one request to the native side and hands the typed reply to + /// . + /// + /// The method name the bridge dispatches on. + /// + /// The parameters, either a model or a built at the call site. Null + /// sends the method alone. + /// + /// + /// Called with the decoded reply, or with the error the reply carried. + /// + /// + /// The public method the request was made from, filled in by the compiler. It names the + /// call in the diagnostic when the app's handler throws. + /// + internal static void Send( + string method, + object request, + Action completionHandler, + [CallerMemberName] string caller = null + ) + { + SendRaw( + method, + request, + (value, error) => + InvokeCompletion(() => completionHandler?.Invoke(value, error), caller) + ); + } + + /// + /// Sends one request whose reply carries no value of its own, and reports only the error. + /// + /// The method name the bridge dispatches on. + /// + /// The parameters, either a model or a built at the call site. Null + /// sends the method alone. + /// + /// Called with the error the reply carried, or null. + /// + /// The public method the request was made from, filled in by the compiler. It names the + /// call in the diagnostic when the app's handler throws. + /// + internal static void SendVoid( + string method, + object request, + Action completionHandler, + [CallerMemberName] string caller = null + ) => + Send( + method, + request, + (value, error) => completionHandler?.Invoke(error), + caller + ); + + /// + /// Names the request that is calling back, and hands the call to the one callback policy. + /// + /// + /// The wrapping itself belongs to - what lives here is + /// only the wording, in one place, so that the 40 requests cannot drift apart. + /// + private static void InvokeCompletion(Action invocation, string caller) => + AdaptyCallbacks.InvokeSafe(invocation, $"Failed to invoke completionHandler in {caller}(..)"); + + /// + /// Reports a request that could not be encoded before it reached , + /// with the error the transport would have produced had the encoding happened inside it. + /// + /// + /// For the one overload that has to serialize an argument of its own before it can build + /// the request. Without this the exception would leave the SDK synchronously, and that one + /// public method would report failure differently from the other forty. + /// + internal static void FailEncoding( + string method, + Exception exception, + Action completionHandler, + [CallerMemberName] string caller = null + ) => + InvokeCompletion( + () => completionHandler?.Invoke(EncodingFailed(method, exception)), + caller + ); + + private static AdaptyError EncodingFailed(string method, Exception exception) => + new AdaptyError( + AdaptyErrorCode.EncodingFailed, + $"Failed encoding request: {method}", + $"AdaptyUnityError.EncodingFailed({exception})" + ); + + private static void SendRaw( + string method, + object request, + Action completionHandler + ) + { + string payload; + try + { + payload = AdaptyJson.SerializeRequest(method, request); + } + catch (Exception ex) + { + completionHandler(default(T), EncodingFailed(method, ex)); + return; + } + + _Adapty.Invoke( + method, + payload, + (json) => + { + var result = AdaptyResponse.Parse(json); + completionHandler(result.Value, result.Error); + } + ); + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyConfiguration+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/AdaptyRequest.cs.meta similarity index 83% rename from Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyConfiguration+JSON.cs.meta rename to Packages/com.adapty.unity-sdk/Runtime/AdaptyRequest.cs.meta index 2ef3a63..67be48a 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyConfiguration+JSON.cs.meta +++ b/Packages/com.adapty.unity-sdk/Runtime/AdaptyRequest.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 7c353ed6c8f2441449d30b24431fb8e1 +guid: c6004a293b224f79aa8b5044ac5054bf MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/com.adapty.unity-sdk/Runtime/Doxyfile b/Packages/com.adapty.unity-sdk/Runtime/Doxyfile index a8fd2f9..dbf5271 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Doxyfile +++ b/Packages/com.adapty.unity-sdk/Runtime/Doxyfile @@ -1095,7 +1095,7 @@ EXCLUDE_PATTERNS = # wildcard * is used, a substring. Examples: ANamespace, AClass, # ANamespace::AClass, ANamespace::*Test -EXCLUDE_SYMBOLS = AdaptySDK.SimpleJSON \ +EXCLUDE_SYMBOLS = AdaptySDK.Serialization \ AdaptySDK.iOS \ AdaptySDK.Android \ AdaptySDK.Noop diff --git a/Packages/com.adapty.unity-sdk/Runtime/IAdaptyEventListener.cs b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyEventListener.cs index 67406c1..b923f1a 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/IAdaptyEventListener.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyEventListener.cs @@ -1,19 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using UnityEngine; -using AdaptySDK.SimpleJSON; -#if UNITY_IOS && !UNITY_EDITOR -using _AdaptyCallbackAction = AdaptySDK.iOS.AdaptyIOSCallbackAction; -#elif UNITY_ANDROID && !UNITY_EDITOR -using _AdaptyCallbackAction = AdaptySDK.Android.AdaptyAndroidCallbackAction; -#else -using _AdaptyCallbackAction = AdaptySDK.Noop.AdaptyNoopCallbackAction; -#endif - namespace AdaptySDK { - /// /// Interface for listening to Adapty SDK events. /// @@ -41,1110 +27,4 @@ public interface IAdaptyEventListener /// The object describing the error. void OnInstallationDetailsFail(AdaptyError error); } - - /// - /// Interface for listening to flow view events. - /// - /// - /// Implement this interface to receive notifications about flow view lifecycle, user actions, purchases, and errors. - /// Use to register your listener. - /// Note that the SDK applies no default behavior to these events: a successful purchase or an error does not dismiss the view automatically — call yourself when appropriate. - /// - public interface IAdaptyFlowsEventsListener - { - /// - /// Called when the flow view appears on screen. - /// - /// The that appeared. - void FlowViewDidAppear(AdaptyUIFlowView view); - - /// - /// Called when the flow view disappears from screen. - /// - /// The that disappeared. - void FlowViewDidDisappear(AdaptyUIFlowView view); - - /// - /// Called when a user performs an action in the flow view (e.g., close, system back, opening a URL, custom actions). - /// - /// - /// The Android system back button is delivered here as a system_back action and does not dismiss the view automatically. - /// To keep the default URL behavior for open_url actions, call . - /// - /// The where the action occurred. - /// The object describing the action. - void FlowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIUserAction action); - - /// - /// Called when a user selects a product in the flow view. - /// - /// The where the selection occurred. - /// The identifier of the selected product. - void FlowViewDidSelectProduct(AdaptyUIFlowView view, string productId); - - /// - /// Called when a purchase is initiated for a product. - /// - /// The where the purchase was initiated. - /// The being purchased. - void FlowViewDidStartPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product); - - /// - /// Called when a purchase is successfully completed. - /// - /// - /// The view is not dismissed automatically — call if desired. - /// - /// The where the purchase was completed. - /// The that was purchased. - /// The object containing purchase details. - void FlowViewDidFinishPurchase( - AdaptyUIFlowView view, - AdaptyPaywallProduct product, - AdaptyPurchaseResult purchasedResult - ); - - /// - /// Called when a purchase fails. - /// - /// The where the purchase failed. - /// The that failed to purchase. - /// The object describing the error. - void FlowViewDidFailPurchase( - AdaptyUIFlowView view, - AdaptyPaywallProduct product, - AdaptyError error - ); - - /// - /// Called when the restore purchases process is initiated. - /// - /// The where the restore was initiated. - void FlowViewDidStartRestore(AdaptyUIFlowView view); - - /// - /// Called when the restore purchases process completes successfully. - /// - /// The where the restore was completed. - /// The updated object containing restored purchases. - void FlowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile); - - /// - /// Called when the restore purchases process fails. - /// - /// The where the restore failed. - /// The object describing the error. - void FlowViewDidFailRestore(AdaptyUIFlowView view, AdaptyError error); - - /// - /// Called when the flow view receives an error (including rendering failures). - /// - /// - /// The view is not dismissed automatically — call if desired. - /// - /// The that received the error. - /// The object describing the error. - void FlowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error); - - /// - /// Called when the flow view fails to load products. - /// - /// The that failed to load products. - /// The object describing the error. - void FlowViewDidFailLoadingProducts(AdaptyUIFlowView view, AdaptyError error); - - /// - /// Called when web payment navigation finishes (for web-based purchases). - /// - /// The where the navigation occurred. - /// The associated with the web payment, or null. - /// The object, or null if no error occurred. - void FlowViewDidFinishWebPaymentNavigation( - AdaptyUIFlowView view, - AdaptyPaywallProduct product, // can be null - AdaptyError error // can be null if no error occurred - ); - - /// - /// Called when the flow view emits a customer-facing analytics event. - /// - /// The where the event occurred. - /// The name of the analytics event. - /// The parameters of the analytics event. - void FlowViewDidReceiveAnalyticEvent( - AdaptyUIFlowView view, - string name, - IDictionary @params - ); - } - - /// - /// Interface for handling system requests initiated by a flow: OS permission prompts and store review requests. - /// - /// - /// Use to register your handler. - /// If no handler is registered, permission requests are ignored (no answer is sent), and app review requests fall back to . - /// - public interface IAdaptyUISystemRequestsHandler - { - /// - /// Called when a flow asks for an OS permission. - /// - /// - /// Request the permission from the OS yourself, then invoke exactly once with the outcome. - /// - /// The that asked for the permission. - /// The permission identifier (e.g., "push", "camera", "tracking"). Unknown values pass through unchanged. - /// Optional custom arguments configured in the Adapty Dashboard, or null. - /// Invoke with the outcome: granted flag and an optional detail string (may be null). - void FlowViewDidAskPermission( - AdaptyUIFlowView view, - string permission, - IDictionary customArgs, - Action respond - ); - - /// - /// Called when a flow requests a native store review prompt. - /// - /// - /// To keep the default behavior, call . - /// - /// The that requested the review. - void FlowViewDidRequestAppReview(AdaptyUIFlowView view); - } - - /// - /// Interface for resolving purchases and restores initiated by a flow while the SDK runs in Observer mode. - /// - /// - /// Use to register your resolver. - /// Read more at Adapty Documentation - /// - public interface IAdaptyUIObserverModeResolver - { - /// - /// Called when a user initiates a purchase in a flow view while the SDK runs in Observer mode. - /// - /// - /// Perform the purchase with your own billing implementation. Invoke when your purchase flow starts and when it finishes (successfully or not). - /// - /// The where the purchase was initiated. - /// The being purchased. - /// Invoke when your purchase flow starts. - /// Invoke when your purchase flow finishes. - void FlowViewDidInitiatePurchase( - AdaptyUIFlowView view, - AdaptyPaywallProduct product, - Action onStartPurchase, - Action onFinishPurchase - ); - - /// - /// Called when a user initiates a restore in a flow view while the SDK runs in Observer mode. - /// - /// - /// Perform the restore with your own billing implementation. Invoke when your restore flow starts and when it finishes (successfully or not). - /// - /// The where the restore was initiated. - /// Invoke when your restore flow starts. - /// Invoke when your restore flow finishes. - void FlowViewDidInitiateRestore( - AdaptyUIFlowView view, - Action onStartRestore, - Action onFinishRestore - ); - } - - /// - /// Interface for listening to onboarding view events. - /// - /// - /// Implement this interface to receive notifications about onboarding view lifecycle, user actions, and analytics events. - /// Use to register your listener. - /// - public interface IAdaptyOnboardingsEventsListener - { - /// - /// Called when the onboarding view fails with an error. - /// - /// The that failed. - /// The object describing the error. - void OnboardingViewDidFailWithError(AdaptyUIOnboardingView view, AdaptyError error); - - /// - /// Called when the onboarding view finishes loading. - /// - /// The that finished loading. - /// The object containing onboarding metadata. - void OnboardingViewDidFinishLoading( - AdaptyUIOnboardingView view, - AdaptyUIOnboardingMeta meta - ); - - /// - /// Called when a close action is triggered in the onboarding view. - /// - /// The where the action occurred. - /// The object containing onboarding metadata. - /// The identifier of the close action. - void OnboardingViewOnCloseAction( - AdaptyUIOnboardingView view, - AdaptyUIOnboardingMeta meta, - string actionId - ); - - /// - /// Called when a paywall action is triggered in the onboarding view. - /// - /// The where the action occurred. - /// The object containing onboarding metadata. - /// The identifier of the paywall action. - void OnboardingViewOnPaywallAction( - AdaptyUIOnboardingView view, - AdaptyUIOnboardingMeta meta, - string actionId - ); - - /// - /// Called when a custom action is triggered in the onboarding view. - /// - /// The where the action occurred. - /// The object containing onboarding metadata. - /// The identifier of the custom action. - void OnboardingViewOnCustomAction( - AdaptyUIOnboardingView view, - AdaptyUIOnboardingMeta meta, - string actionId - ); - - /// - /// Called when the state of an element in the onboarding view is updated. - /// - /// The where the update occurred. - /// The object containing onboarding metadata. - /// The identifier of the element whose state was updated. - /// The object containing the updated state parameters. - void OnboardingViewOnStateUpdatedAction( - AdaptyUIOnboardingView view, - AdaptyUIOnboardingMeta meta, - string elementId, - AdaptyOnboardingsStateUpdatedParams @params - ); - - /// - /// Called when an analytics event is triggered in the onboarding view. - /// - /// The where the event occurred. - /// The object containing onboarding metadata. - /// The object containing analytics event data. - void OnboardingViewOnAnalyticsEvent( - AdaptyUIOnboardingView view, - AdaptyUIOnboardingMeta meta, - AdaptyOnboardingsAnalyticsEvent analyticsEvent - ); - } - - public static partial class Adapty - { - private static IAdaptyEventListener m_Listener; - private static IAdaptyFlowsEventsListener m_FlowsEventsListener; - private static IAdaptyOnboardingsEventsListener m_OnboardingsEventsListener; - private static IAdaptyUISystemRequestsHandler m_SystemRequestsHandler; - private static IAdaptyUIObserverModeResolver m_ObserverModeResolver; - - /// - /// Sets the event listener for Adapty SDK events. - /// - /// The implementation to receive events, or null to detach the previous one. - public static void SetEventListener(IAdaptyEventListener listener) - { - _AdaptyCallbackAction.InitializeOnce(); - m_Listener = listener; - } - - /// - /// Sets the event listener for flow view events. - /// - /// The implementation to receive events, or null to detach the previous one. - public static void SetFlowsEventsListener(IAdaptyFlowsEventsListener listener) - { - _AdaptyCallbackAction.InitializeOnce(); - m_FlowsEventsListener = listener; - } - - /// - /// Sets the handler for system requests initiated by a flow (OS permission prompts and store review requests). - /// - /// The implementation to receive requests, or null to detach the previous one. - public static void SetSystemRequestsHandler(IAdaptyUISystemRequestsHandler handler) - { - _AdaptyCallbackAction.InitializeOnce(); - m_SystemRequestsHandler = handler; - } - - /// - /// Sets the resolver for purchases and restores initiated by a flow while the SDK runs in Observer mode. - /// - /// The implementation to resolve purchases and restores, or null to detach the previous one. - public static void SetObserverModeResolver(IAdaptyUIObserverModeResolver resolver) - { - _AdaptyCallbackAction.InitializeOnce(); - m_ObserverModeResolver = resolver; - } - - /// - /// Sets the event listener for onboarding view events. - /// - /// The implementation to receive events, or null to detach the previous one. - [Obsolete( - "The legacy onboarding API is deprecated in favor of Flows. Use SetFlowsEventsListener instead." - )] - public static void SetOnboardingsEventsListener(IAdaptyOnboardingsEventsListener listener) - { - _AdaptyCallbackAction.InitializeOnce(); - m_OnboardingsEventsListener = listener; - } - - private static bool RequireEventListener(string eventId) - { - if (m_Listener == null) - { - Debug.LogWarning( - string.Format( - "[Adapty] Event listener is not set, ignoring event '{0}'. Call Adapty.SetEventListener() to receive events.", - eventId - ) - ); - return false; - } - return true; - } - - private static bool RequireFlowsListener(string eventId) - { - if (m_FlowsEventsListener == null) - { - Debug.LogWarning( - string.Format( - "[Adapty] Flows events listener is not set, ignoring event '{0}'. Call Adapty.SetFlowsEventsListener() to receive flow events.", - eventId - ) - ); - return false; - } - return true; - } - - private static bool RequireOnboardingsListener(string eventId) - { - if (m_OnboardingsEventsListener == null) - { - Debug.LogWarning( - string.Format( - "[Adapty] Onboardings events listener is not set, ignoring event '{0}'. Call Adapty.SetOnboardingsEventsListener() to receive onboarding events.", - eventId - ) - ); - return false; - } - return true; - } - - internal static void OnMessage(string id, string json) - { - if (string.IsNullOrEmpty(json)) - return; - - JSONNode response; - try - { - response = JSONNode.Parse(json); - } - catch (Exception e) - { - Debug.LogError( - string.Format( - "[Adapty] Failed to parse event JSON for event '{0}': {1}", - id ?? "(null)", - e.Message - ) - ); - return; - } - - if (response == null || response.IsNull) - { - return; - } - - if (!response.IsObject) - { - return; - } - - var parameters = response.AsObject; - switch (id) - { - case "did_load_latest_profile": - { - if (!RequireEventListener(id)) - return; - var profile = parameters.GetAdaptyProfile("profile"); - try - { - m_Listener.OnLoadLatestProfile(profile); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyEventListener.OnLoadLatestProfile(..)", - e - ); - } - return; - } - case "on_installation_details_success": - { - if (!RequireEventListener(id)) - return; - var details = parameters.GetAdaptyInstallationDetails("details"); - try - { - m_Listener.OnInstallationDetailsSuccess(details); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyEventListener.OnInstallationDetailsSuccess(..)", - e - ); - } - return; - } - case "on_installation_details_fail": - { - if (!RequireEventListener(id)) - return; - var error = parameters.GetAdaptyError("error"); - try - { - m_Listener.OnInstallationDetailsFail(error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyEventListener.OnInstallationDetailsFail(..)", - e - ); - } - return; - } - case "onboarding_did_fail_with_error": - { - if (!RequireOnboardingsListener(id)) - return; - var view = parameters.GetAdaptyUIOnboardingView("view"); - var error = parameters.GetAdaptyError("error"); - try - { - m_OnboardingsEventsListener.OnboardingViewDidFailWithError(view, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewDidFailWithError(..)", - e - ); - } - return; - } - case "onboarding_on_analytics_action": - { - if (!RequireOnboardingsListener(id)) - return; - var view = parameters.GetAdaptyUIOnboardingView("view"); - var meta = parameters.GetAdaptyUIOnboardingMeta("meta"); - var ev = parameters.GetOnboardingsAnalyticsEvent("event"); - try - { - m_OnboardingsEventsListener.OnboardingViewOnAnalyticsEvent(view, meta, ev); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewOnAnalyticsEvent(..)", - e - ); - } - return; - } - case "onboarding_did_finish_loading": - { - if (!RequireOnboardingsListener(id)) - return; - var view = parameters.GetAdaptyUIOnboardingView("view"); - var meta = parameters.GetAdaptyUIOnboardingMeta("meta"); - try - { - m_OnboardingsEventsListener.OnboardingViewDidFinishLoading(view, meta); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewDidFinishLoading(..)", - e - ); - } - return; - } - case "onboarding_on_close_action": - { - if (!RequireOnboardingsListener(id)) - return; - var view = parameters.GetAdaptyUIOnboardingView("view"); - var meta = parameters.GetAdaptyUIOnboardingMeta("meta"); - var actionId = parameters.GetString("action_id"); - try - { - m_OnboardingsEventsListener.OnboardingViewOnCloseAction( - view, - meta, - actionId - ); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewOnCloseAction(..)", - e - ); - } - return; - } - case "onboarding_on_paywall_action": - { - if (!RequireOnboardingsListener(id)) - return; - var view = parameters.GetAdaptyUIOnboardingView("view"); - var meta = parameters.GetAdaptyUIOnboardingMeta("meta"); - var actionId = parameters.GetString("action_id"); - try - { - m_OnboardingsEventsListener.OnboardingViewOnPaywallAction( - view, - meta, - actionId - ); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewOnPaywallAction(..)", - e - ); - } - return; - } - case "onboarding_on_custom_action": - { - if (!RequireOnboardingsListener(id)) - return; - var view = parameters.GetAdaptyUIOnboardingView("view"); - var meta = parameters.GetAdaptyUIOnboardingMeta("meta"); - var actionId = parameters.GetString("action_id"); - try - { - m_OnboardingsEventsListener.OnboardingViewOnCustomAction( - view, - meta, - actionId - ); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewOnCustomAction(..)", - e - ); - } - return; - } - case "onboarding_on_state_updated_action": - { - if (!RequireOnboardingsListener(id)) - return; - var view = parameters.GetAdaptyUIOnboardingView("view"); - var meta = parameters.GetAdaptyUIOnboardingMeta("meta"); - var elementId = JSONNodeExtensions - .GetObject(parameters, "action") - .GetString("element_id"); - var @params = parameters.GetOnboardingsStateUpdatedParams("action"); - try - { - m_OnboardingsEventsListener.OnboardingViewOnStateUpdatedAction( - view, - meta, - elementId, - @params - ); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewOnStateUpdatedAction(..)", - e - ); - } - return; - } - case "flow_view_did_appear": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - try - { - m_FlowsEventsListener.FlowViewDidAppear(view); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidAppear(..)", - e - ); - } - return; - } - case "flow_view_did_disappear": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - try - { - m_FlowsEventsListener.FlowViewDidDisappear(view); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidDisappear(..)", - e - ); - } - return; - } - case "flow_view_did_perform_action": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - var action = parameters.GetAdaptyUIUserAction("action"); - try - { - m_FlowsEventsListener.FlowViewDidPerformAction(view, action); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidPerformAction(..)", - e - ); - } - return; - } - case "flow_view_did_select_product": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - var productId = parameters.GetString("product_id"); - try - { - m_FlowsEventsListener.FlowViewDidSelectProduct(view, productId); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidSelectProduct(..)", - e - ); - } - return; - } - case "flow_view_did_start_purchase": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - var product = parameters.GetAdaptyPaywallProduct("product"); - try - { - m_FlowsEventsListener.FlowViewDidStartPurchase(view, product); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidStartPurchase(..)", - e - ); - } - return; - } - case "flow_view_did_finish_purchase": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - var product = parameters.GetAdaptyPaywallProduct("product"); - var purchaseResult = parameters.GetAdaptyPurchaseResult("purchased_result"); - try - { - m_FlowsEventsListener.FlowViewDidFinishPurchase( - view, - product, - purchaseResult - ); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFinishPurchase(..)", - e - ); - } - return; - } - case "flow_view_did_fail_purchase": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - var product = parameters.GetAdaptyPaywallProduct("product"); - var error = parameters.GetAdaptyError("error"); - try - { - m_FlowsEventsListener.FlowViewDidFailPurchase(view, product, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFailPurchase(..)", - e - ); - } - return; - } - case "flow_view_did_start_restore": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - try - { - m_FlowsEventsListener.FlowViewDidStartRestore(view); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidStartRestore(..)", - e - ); - } - return; - } - case "flow_view_did_finish_restore": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - var profile = parameters.GetAdaptyProfile("profile"); - try - { - m_FlowsEventsListener.FlowViewDidFinishRestore(view, profile); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFinishRestore(..)", - e - ); - } - return; - } - case "flow_view_did_fail_restore": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - var error = parameters.GetAdaptyError("error"); - try - { - m_FlowsEventsListener.FlowViewDidFailRestore(view, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFailRestore(..)", - e - ); - } - return; - } - case "flow_view_did_receive_error": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - var error = parameters.GetAdaptyError("error"); - try - { - m_FlowsEventsListener.FlowViewDidReceiveError(view, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidReceiveError(..)", - e - ); - } - return; - } - case "flow_view_did_fail_loading_products": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - var error = parameters.GetAdaptyError("error"); - try - { - m_FlowsEventsListener.FlowViewDidFailLoadingProducts(view, error); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFailLoadingProducts(..)", - e - ); - } - return; - } - case "flow_view_did_finish_web_payment_navigation": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - var product = parameters.GetAdaptyPaywallProductIfPresent("product"); - var error = parameters.GetAdaptyErrorIfPresent("error"); - try - { - m_FlowsEventsListener.FlowViewDidFinishWebPaymentNavigation( - view, - product, - error - ); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidFinishWebPaymentNavigation(..)", - e - ); - } - return; - } - case "flow_view_did_receive_analytic_event": - { - if (!RequireFlowsListener(id)) - return; - var view = parameters.GetAdaptyUIFlowView("view"); - var name = parameters.GetString("name"); - var @params = parameters.GetDictionary("params"); - try - { - m_FlowsEventsListener.FlowViewDidReceiveAnalyticEvent( - view, - name, - @params - ); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyFlowsEventsListener.FlowViewDidReceiveAnalyticEvent(..)", - e - ); - } - return; - } - case "flow_view_did_ask_permission": - { - var view = parameters.GetAdaptyUIFlowView("view"); - var eventId = parameters.GetString("event_id"); - var permission = parameters.GetString("permission"); - - Dictionary customArgs = null; - var customArgsObject = JSONNodeExtensions.GetObjectIfPresent( - parameters, - "custom_args" - ); - if (customArgsObject != null) - { - customArgs = new Dictionary(); - foreach (KeyValuePair pair in customArgsObject) - { - customArgs[pair.Key] = pair.Value.Value; - } - } - - if (m_SystemRequestsHandler == null) - { - // Send no answer: the native HostRequestRegistry keeps the request pending - // until the view tears down, then resolves it as denied there. Fabricating an - // answer here would duplicate that fallback across two layers — this matches - // both the native no-handler behavior and the Flutter SDK. - Debug.LogWarning( - string.Format( - "[Adapty] System requests handler is not set, ignoring permission request '{0}'. Call Adapty.SetSystemRequestsHandler() to handle permission requests.", - permission - ) - ); - return; - } - - // respond(..) is typically invoked from an OS permission callback, off the main thread. - var answered = 0; - Action respond = (granted, detail) => - { - if (Interlocked.Exchange(ref answered, 1) == 1) - { - Debug.LogWarning( - "[Adapty] Permission request has already been answered, ignoring subsequent respond(..) call." - ); - return; - } - AdaptyUI.FlowViewAnswerPermission(eventId, granted, detail); - }; - - try - { - m_SystemRequestsHandler.FlowViewDidAskPermission( - view, - permission, - customArgs, - respond - ); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyUISystemRequestsHandler.FlowViewDidAskPermission(..)", - e - ); - } - return; - } - case "flow_view_did_request_app_review": - { - var view = parameters.GetAdaptyUIFlowView("view"); - - if (m_SystemRequestsHandler == null) - { - AdaptyUI.RequestAppReview(null); - return; - } - - try - { - m_SystemRequestsHandler.FlowViewDidRequestAppReview(view); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyUISystemRequestsHandler.FlowViewDidRequestAppReview(..)", - e - ); - } - return; - } - case "flow_view_observer_did_initiate_purchase": - { - var view = parameters.GetAdaptyUIFlowView("view"); - var eventId = parameters.GetString("event_id"); - var product = parameters.GetAdaptyPaywallProduct("product"); - - if (m_ObserverModeResolver == null) - { - Debug.LogWarning( - "[Adapty] Observer mode resolver is not set, ignoring initiated purchase. Call Adapty.SetObserverModeResolver() to handle purchases in Observer mode." - ); - return; - } - - Action onStartPurchase = () => - AdaptyUI.SendObserverEvent("observer_purchase_did_start", eventId); - Action onFinishPurchase = () => - AdaptyUI.SendObserverEvent("observer_purchase_did_finish", eventId); - - try - { - m_ObserverModeResolver.FlowViewDidInitiatePurchase( - view, - product, - onStartPurchase, - onFinishPurchase - ); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyUIObserverModeResolver.FlowViewDidInitiatePurchase(..)", - e - ); - } - return; - } - case "flow_view_observer_did_initiate_restore": - { - var view = parameters.GetAdaptyUIFlowView("view"); - var eventId = parameters.GetString("event_id"); - - if (m_ObserverModeResolver == null) - { - Debug.LogWarning( - "[Adapty] Observer mode resolver is not set, ignoring initiated restore. Call Adapty.SetObserverModeResolver() to handle restores in Observer mode." - ); - return; - } - - Action onStartRestore = () => - AdaptyUI.SendObserverEvent("observer_restore_did_start", eventId); - Action onFinishRestore = () => - AdaptyUI.SendObserverEvent("observer_restore_did_finish", eventId); - - try - { - m_ObserverModeResolver.FlowViewDidInitiateRestore( - view, - onStartRestore, - onFinishRestore - ); - } - catch (Exception e) - { - throw new Exception( - "Failed to invoke IAdaptyUIObserverModeResolver.FlowViewDidInitiateRestore(..)", - e - ); - } - return; - } - default: - Debug.LogWarning( - string.Format("[Adapty] Unknown event id '{0}', ignoring.", id ?? "(null)") - ); - return; - } - } - } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/IAdaptyFlowsEventsListener.cs b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyFlowsEventsListener.cs new file mode 100644 index 0000000..eae096b --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyFlowsEventsListener.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; + +namespace AdaptySDK +{ + /// + /// Interface for listening to flow view events. + /// + /// + /// Implement this interface to receive notifications about flow view lifecycle, user actions, purchases, and errors. + /// Use to register your listener. + /// Note that the SDK applies no default behavior to these events: a successful purchase or an error does not dismiss the view automatically — call yourself when appropriate. + /// + public interface IAdaptyFlowsEventsListener + { + /// + /// Called when the flow view appears on screen. + /// + /// The that appeared. + void FlowViewDidAppear(AdaptyUIFlowView view); + + /// + /// Called when the flow view disappears from screen. + /// + /// The that disappeared. + void FlowViewDidDisappear(AdaptyUIFlowView view); + + /// + /// Called when a user performs an action in the flow view (e.g., close, system back, opening a URL, custom actions). + /// + /// + /// The Android system back button is delivered here as a system_back action and does not dismiss the view automatically. + /// To keep the default URL behavior for open_url actions, call . + /// + /// The where the action occurred. + /// The object describing the action. + void FlowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIUserAction action); + + /// + /// Called when a user selects a product in the flow view. + /// + /// The where the selection occurred. + /// The identifier of the selected product. + void FlowViewDidSelectProduct(AdaptyUIFlowView view, string productId); + + /// + /// Called when a purchase is initiated for a product. + /// + /// The where the purchase was initiated. + /// The being purchased. + void FlowViewDidStartPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product); + + /// + /// Called when a purchase is successfully completed. + /// + /// + /// The view is not dismissed automatically — call if desired. + /// + /// The where the purchase was completed. + /// The that was purchased. + /// The object containing purchase details. + void FlowViewDidFinishPurchase( + AdaptyUIFlowView view, + AdaptyPaywallProduct product, + AdaptyPurchaseResult purchasedResult + ); + + /// + /// Called when a purchase fails. + /// + /// The where the purchase failed. + /// The that failed to purchase. + /// The object describing the error. + void FlowViewDidFailPurchase( + AdaptyUIFlowView view, + AdaptyPaywallProduct product, + AdaptyError error + ); + + /// + /// Called when the restore purchases process is initiated. + /// + /// The where the restore was initiated. + void FlowViewDidStartRestore(AdaptyUIFlowView view); + + /// + /// Called when the restore purchases process completes successfully. + /// + /// The where the restore was completed. + /// The updated object containing restored purchases. + void FlowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile); + + /// + /// Called when the restore purchases process fails. + /// + /// The where the restore failed. + /// The object describing the error. + void FlowViewDidFailRestore(AdaptyUIFlowView view, AdaptyError error); + + /// + /// Called when the flow view receives an error (including rendering failures). + /// + /// + /// The view is not dismissed automatically — call if desired. + /// + /// The that received the error. + /// The object describing the error. + void FlowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error); + + /// + /// Called when the flow view fails to load products. + /// + /// The that failed to load products. + /// The object describing the error. + void FlowViewDidFailLoadingProducts(AdaptyUIFlowView view, AdaptyError error); + + /// + /// Called when web payment navigation finishes (for web-based purchases). + /// + /// The where the navigation occurred. + /// The associated with the web payment, or null. + /// The object, or null if no error occurred. + void FlowViewDidFinishWebPaymentNavigation( + AdaptyUIFlowView view, + AdaptyPaywallProduct product, + AdaptyError error + ); + + /// + /// Called when the flow view emits a customer-facing analytics event. + /// + /// The where the event occurred. + /// The name of the analytics event. + /// The parameters of the analytics event. + void FlowViewDidReceiveAnalyticEvent( + AdaptyUIFlowView view, + string name, + IReadOnlyDictionary parameters + ); + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyLogLevel+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyFlowsEventsListener.cs.meta similarity index 83% rename from Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyLogLevel+JSON.cs.meta rename to Packages/com.adapty.unity-sdk/Runtime/IAdaptyFlowsEventsListener.cs.meta index 6c9f358..6dd5d40 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyLogLevel+JSON.cs.meta +++ b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyFlowsEventsListener.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 428d844db41ef4186bfb4cf0e2a214c4 +guid: bbad9ff2841547729115aa5e0dc7d2b0 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUIObserverModeResolver.cs b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUIObserverModeResolver.cs new file mode 100644 index 0000000..77bddf2 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUIObserverModeResolver.cs @@ -0,0 +1,46 @@ +using System; + +namespace AdaptySDK +{ + /// + /// Interface for resolving purchases and restores initiated by a flow while the SDK runs in Observer mode. + /// + /// + /// Use to register your resolver. + /// Read more at Adapty Documentation + /// + public interface IAdaptyUIObserverModeResolver + { + /// + /// Called when a user initiates a purchase in a flow view while the SDK runs in Observer mode. + /// + /// + /// Perform the purchase with your own billing implementation. Invoke when your purchase flow starts and when it finishes (successfully or not). Both are safe to invoke from any thread - the SDK sends the report from the Unity main thread. + /// + /// The where the purchase was initiated. + /// The being purchased. + /// Invoke when your purchase flow starts. + /// Invoke when your purchase flow finishes. + void FlowViewDidInitiatePurchase( + AdaptyUIFlowView view, + AdaptyPaywallProduct product, + Action onStartPurchase, + Action onFinishPurchase + ); + + /// + /// Called when a user initiates a restore in a flow view while the SDK runs in Observer mode. + /// + /// + /// Perform the restore with your own billing implementation. Invoke when your restore flow starts and when it finishes (successfully or not). Both are safe to invoke from any thread - the SDK sends the report from the Unity main thread. + /// + /// The where the restore was initiated. + /// Invoke when your restore flow starts. + /// Invoke when your restore flow finishes. + void FlowViewDidInitiateRestore( + AdaptyUIFlowView view, + Action onStartRestore, + Action onFinishRestore + ); + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUIObserverModeResolver.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUIObserverModeResolver.cs.meta new file mode 100644 index 0000000..35ba94c --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUIObserverModeResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a65b2afae4dd4dcd8eeb2b37a5a42bca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUISystemRequestsHandler.cs b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUISystemRequestsHandler.cs new file mode 100644 index 0000000..006436d --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUISystemRequestsHandler.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace AdaptySDK +{ + /// + /// Interface for handling system requests initiated by a flow: OS permission prompts and store review requests. + /// + /// + /// Use to register your handler. + /// If no handler is registered, permission requests are ignored (no answer is sent), and app review requests fall back to . + /// + public interface IAdaptyUISystemRequestsHandler + { + /// + /// Called when a flow asks for an OS permission. + /// + /// + /// Request the permission from the OS yourself, then invoke exactly once with the outcome. + /// + /// The that asked for the permission. + /// The permission identifier (e.g., "push", "camera", "tracking"). Unknown values pass through unchanged. + /// Optional custom arguments configured in the Adapty Dashboard, or null. + /// Invoke with the outcome: granted flag and an optional detail string (may be null). Safe to invoke from any thread - the SDK sends the answer from the Unity main thread. + void FlowViewDidAskPermission( + AdaptyUIFlowView view, + string permission, + IReadOnlyDictionary customArgs, + Action respond + ); + + /// + /// Called when a flow requests a native store review prompt. + /// + /// + /// To keep the default behavior, call . + /// + /// The that requested the review. + void FlowViewDidRequestAppReview(AdaptyUIFlowView view); + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUISystemRequestsHandler.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUISystemRequestsHandler.cs.meta new file mode 100644 index 0000000..d733291 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/IAdaptyUISystemRequestsHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 87ff9c35916548949b11c182e937d79b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyConfiguration+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyConfiguration+JSON.cs deleted file mode 100644 index f54a335..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyConfiguration+JSON.cs +++ /dev/null @@ -1,60 +0,0 @@ -// -// AdaptyConfiguration+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 10.12.2024. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyConfiguration - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("api_key", ApiKey); - - if (CustomerUserId != null) - node.Add("customer_user_id", CustomerUserId); - if (CustomerIdentity != null && !CustomerIdentity.IsEmpty) - node.Add("customer_identity_parameters", CustomerIdentity.ToJSONNode()); - if (ObserverMode != null) - node.Add("observer_mode", ObserverMode); -#if ADAPTY_KIDS_MODE && UNITY_IOS - // The KidsMode trait compiles IDFA out of the binary; keep the config in sync. - node.Add("apple_idfa_collection_disabled", true); -#else - if (AppleIdfaCollectionDisabled != null) - node.Add("apple_idfa_collection_disabled", AppleIdfaCollectionDisabled); -#endif - if (GoogleAdvertisingIdCollectionDisabled != null) - node.Add("google_adid_collection_disabled", GoogleAdvertisingIdCollectionDisabled); - if (GoogleEnablePendingPrepaidPlans != null) - node.Add("google_enable_pending_prepaid_plans", GoogleEnablePendingPrepaidPlans); - if (GoogleLocalAccessLevelAllowed != null) - node.Add("google_local_access_level_allowed", GoogleLocalAccessLevelAllowed); - if (IpAddressCollectionDisabled != null) - node.Add("ip_address_collection_disabled", IpAddressCollectionDisabled); - if (AppleClearDataOnBackup != null) - node.Add("clear_data_on_backup", AppleClearDataOnBackup); - if (LogLevel != null) - node.Add("log_level", LogLevel.Value.ToJSONNode()); - if (ServerCluster != null) - node.Add("server_cluster", ServerCluster.Value.ToJSONNode()); - if (BackendProxyHost != null) - node.Add("backend_proxy_host", BackendProxyHost); - if (BackendProxyPort != null) - node.Add("backend_proxy_port", BackendProxyPort); - if (ActivateUI != null) - node.Add("activate_ui", ActivateUI); - if (AdaptyUIMediaCache != null) - node.Add("media_cache", AdaptyUIMediaCache.ToJSONNode()); - - node.Add("cross_platform_sdk_name", "unity"); - node.Add("cross_platform_sdk_version", Adapty.SDKVersion); - return node; - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomAsset+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomAsset+JSON.cs deleted file mode 100644 index 5a85984..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomAsset+JSON.cs +++ /dev/null @@ -1,203 +0,0 @@ -// -// AdaptyCustomAsset+JSON.cs -// AdaptySDK -// -// Created by Assistant on 14.01.2025. -// - -using System; -using System.Collections.Generic; -using AdaptySDK.SimpleJSON; -using UnityEngine; - -namespace AdaptySDK -{ - public partial class AdaptyCustomAsset - { - internal abstract JSONNode ToJSONNode(); - } - - public partial class AdaptyCustomAssetLocalImageData - { - internal override JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("type", "image"); - node.Add("value", Convert.ToBase64String(Data)); - return node; - } - } - - public partial class AdaptyCustomAssetLocalImageAsset - { - internal override JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("type", "image"); - node.Add("asset_id", AssetId); - return node; - } - } - - public partial class AdaptyCustomAssetLocalImageFile - { - internal override JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("type", "image"); - - // Use the same platform-specific path construction as SetFallback -#if UNITY_IOS && !UNITY_EDITOR - node.Add("path", UnityEngine.Application.dataPath + "/Raw/" + Path); -#elif UNITY_ANDROID && !UNITY_EDITOR - node.Add("path", "jar:file://" + UnityEngine.Application.dataPath + "!/assets/" + Path); -#else - // For editor and other platforms, use the path as-is - node.Add("path", Path); -#endif - return node; - } - } - - public partial class AdaptyCustomAssetLocalVideoAsset - { - internal override JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("type", "video"); - node.Add("asset_id", AssetId); - return node; - } - } - - public partial class AdaptyCustomAssetLocalVideoFile - { - internal override JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("type", "video"); - - // Use the same platform-specific path construction as SetFallback -#if UNITY_IOS && !UNITY_EDITOR - node.Add("path", UnityEngine.Application.dataPath + "/Raw/" + Path); -#elif UNITY_ANDROID && !UNITY_EDITOR - node.Add("path", "jar:file://" + UnityEngine.Application.dataPath + "!/assets/" + Path); -#else - // For editor and other platforms, use the path as-is - node.Add("path", Path); -#endif - return node; - } - } - - public partial class AdaptyCustomAssetColor - { - internal override JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("type", "color"); - node.Add("value", ColorToHex(ColorValue)); - return node; - } - - private static string ColorToHex(Color color) - { - var r = Mathf.RoundToInt(color.r * 255); - var g = Mathf.RoundToInt(color.g * 255); - var b = Mathf.RoundToInt(color.b * 255); - var a = Mathf.RoundToInt(color.a * 255); - - return $"#{r:X2}{g:X2}{b:X2}{a:X2}"; - } - } - - public partial class AdaptyCustomAssetLinearGradient - { - internal override JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("type", "linear-gradient"); - - var values = new JSONArray(); - foreach (var time in KeyTimes()) - { - var valueNode = new JSONObject(); - valueNode.Add("color", ColorToHex(Gradient.Evaluate(time))); - valueNode.Add("p", time); - values.Add(valueNode); - } - node.Add("values", values); - - var pointsNode = new JSONObject(); - pointsNode.Add("x0", 0.0f); // Unity gradients start at 0 - pointsNode.Add("y0", 0.0f); - pointsNode.Add("x1", 1.0f); // Unity gradients end at 1 - pointsNode.Add("y1", 0.0f); - node.Add("points", pointsNode); - - return node; - } - - /// - /// Color keys and alpha keys are independent in a Unity Gradient: they may differ in count and sit - /// at different times. Emit a stop at every key time of either channel and let Gradient.Evaluate - /// resolve the RGBA there, so the serialized gradient matches what Unity renders. - /// - private List KeyTimes() - { - var times = new List(); - - foreach (var key in Gradient.colorKeys) - { - if (!times.Contains(key.time)) - { - times.Add(key.time); - } - } - - foreach (var key in Gradient.alphaKeys) - { - if (!times.Contains(key.time)) - { - times.Add(key.time); - } - } - - times.Sort(); - return times; - } - - private static string ColorToHex(Color color) - { - var r = Mathf.RoundToInt(color.r * 255); - var g = Mathf.RoundToInt(color.g * 255); - var b = Mathf.RoundToInt(color.b * 255); - var a = Mathf.RoundToInt(color.a * 255); - - return $"#{r:X2}{g:X2}{b:X2}{a:X2}"; - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static JSONNode ToJSONNode(this AdaptyCustomAsset customAsset) - { - return customAsset.ToJSONNode(); - } - - internal static JSONNode ToJSONNode(this Dictionary customAssets) - { - var array = new JSONArray(); - foreach (var kvp in customAssets) - { - var assetNode = kvp.Value.ToJSONNode(); - assetNode["id"] = kvp.Key; - array.Add(assetNode); - } - return array; - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomAsset+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomAsset+JSON.cs.meta deleted file mode 100644 index e0d6786..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomAsset+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: b10ee6878335b4eecbe2284edfcc35f9 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomerIdentity+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomerIdentity+JSON.cs deleted file mode 100644 index fe3d119..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomerIdentity+JSON.cs +++ /dev/null @@ -1,42 +0,0 @@ -// -// AdaptyCustomerIdentity+JSON.cs -// AdaptySDK -// -// Created by AI Assistant on 14.01.2025. -// - -using System; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyCustomerIdentity - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - - if (IosAppAccountToken != Guid.Empty) - { - node.Add(_CustomerIdentityKeys.IosAppAccountToken, IosAppAccountToken.ToString()); - } - - if (!string.IsNullOrEmpty(AndroidObfuscatedAccountId)) - { - node.Add( - _CustomerIdentityKeys.AndroidObfuscatedAccountId, - AndroidObfuscatedAccountId - ); - } - - return node; - } - } - - internal static class _CustomerIdentityKeys - { - internal const string IosAppAccountToken = "app_account_token"; - internal const string AndroidObfuscatedAccountId = "obfuscated_account_id"; - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomerIdentity+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomerIdentity+JSON.cs.meta deleted file mode 100644 index 6f8d12e..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyCustomerIdentity+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d7d128af280994c30898cdd09519c3c9 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyError+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyError+JSON.cs deleted file mode 100644 index 529e61f..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyError+JSON.cs +++ /dev/null @@ -1,38 +0,0 @@ -// -// AdaptyError+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - public partial class AdaptyError - { - internal AdaptyError(JSONObject jsonNode) - { - Message = jsonNode.GetString("message"); - Detail = jsonNode.GetStringIfPresent("detail"); - Code = (AdaptyErrorCode)jsonNode.GetInteger("adapty_code"); - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyError GetAdaptyError(this JSONNode node, string aKey) => - new AdaptyError(GetObject(node, aKey)); - - internal static AdaptyError GetAdaptyErrorIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) return null; - return new AdaptyError(obj); - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlow+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlow+JSON.cs deleted file mode 100644 index 59e01df..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlow+JSON.cs +++ /dev/null @@ -1,85 +0,0 @@ -// -// AdaptyFlow+JSON.cs -// AdaptySDK -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyFlow - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - - node.Add("placement", Placement.ToJSONNode()); - node.Add("flow_id", InstanceIdentity); - node.Add("flow_name", Name); - node.Add("variation_id", VariationId); - node.Add("response_created_at", _ResponseCreatedAt); - - if (FlowVersionId != null) - { - node.Add("flow_version_id", FlowVersionId); - } - - if (RemoteConfigs.Count > 0) - { - var remoteConfigs = new JSONArray(); - foreach (var item in RemoteConfigs) - { - remoteConfigs.Add(item.ToJSONNode()); - } - node.Add("remote_configs", remoteConfigs); - } - - var variations = new JSONArray(); - foreach (var item in Paywalls) - { - variations.Add(item.ToJSONNode()); - } - - node.Add("variations", variations); - - if (_PayloadData != null) - { - node.Add("payload_data", _PayloadData); - } - - return node; - } - - internal AdaptyFlow(JSONObject jsonNode) - { - Placement = jsonNode.GetPlacement("placement"); - InstanceIdentity = jsonNode.GetString("flow_id"); - Name = jsonNode.GetString("flow_name"); - VariationId = jsonNode.GetString("variation_id"); - _ResponseCreatedAt = jsonNode.GetLong("response_created_at"); - FlowVersionId = jsonNode.GetStringIfPresent("flow_version_id"); - RemoteConfigs = jsonNode.GetRemoteConfigList("remote_configs"); - Paywalls = jsonNode.GetAdaptyFlowPaywallList("variations"); - _PayloadData = jsonNode.GetStringIfPresent("payload_data"); - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyFlow GetFlow(this JSONNode node) => new AdaptyFlow(GetObject(node)); - - internal static AdaptyFlow GetFlow(this JSONNode node, string aKey) => - new AdaptyFlow(GetObject(node, aKey)); - - internal static AdaptyFlow GetFlowIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) - return null; - return new AdaptyFlow(obj); - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlow+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlow+JSON.cs.meta deleted file mode 100644 index 7d3bfe6..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlow+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a72a477407cd64bbf847b34abeba62cb \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall+JSON.cs deleted file mode 100644 index aa5ee36..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall+JSON.cs +++ /dev/null @@ -1,89 +0,0 @@ -// -// AdaptyFlowPaywall+JSON.cs -// AdaptySDK -// - -using System; -using System.Collections.Generic; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyFlowPaywall - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - - node.Add("placement", Placement.ToJSONNode()); - node.Add("paywall_id", InstanceIdentity); - node.Add("paywall_name", Name); - node.Add("variation_id", VariationId); - - var products = new JSONArray(); - foreach (var item in _Products) - { - products.Add(item.ToJSONNode()); - } - - node.Add("products", products); - - if (_WebPurchaseUrl != null) - { - node.Add("web_purchase_url", _WebPurchaseUrl); - } - - return node; - } - - internal AdaptyFlowPaywall(JSONObject jsonNode) - { - Placement = jsonNode.GetPlacement("placement"); - InstanceIdentity = jsonNode.GetString("paywall_id"); - Name = jsonNode.GetString("paywall_name"); - VariationId = jsonNode.GetString("variation_id"); - _Products = jsonNode.GetAdaptyFlowPaywallProductReferenceList("products"); - _WebPurchaseUrl = jsonNode.GetStringIfPresent("web_purchase_url"); - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyFlowPaywall GetAdaptyFlowPaywall(this JSONNode node) => - new AdaptyFlowPaywall(GetObject(node)); - - internal static AdaptyFlowPaywall GetAdaptyFlowPaywall(this JSONNode node, string aKey) => - new AdaptyFlowPaywall(GetObject(node, aKey)); - - internal static AdaptyFlowPaywall GetAdaptyFlowPaywallIfPresent( - this JSONNode node, - string aKey - ) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) - return null; - return new AdaptyFlowPaywall(obj); - } - - internal static IList GetAdaptyFlowPaywallList( - this JSONNode node, - string aKey - ) - { - var array = GetArray(node, aKey); - var result = new List(); - foreach (var item in array.Children) - { - if (!item.IsObject) - throw new Exception($"Value by index: {result.Count} is not Object"); - result.Add(new AdaptyFlowPaywall(item.AsObject)); - } - return result; - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall+JSON.cs.meta deleted file mode 100644 index 09974b2..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 9ed8c95b6d409454c83832ea88202d95 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall.ProductReference+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall.ProductReference+JSON.cs deleted file mode 100644 index b431961..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall.ProductReference+JSON.cs +++ /dev/null @@ -1,108 +0,0 @@ -// -// AdaptyFlowPaywall.ProductReference+JSON.cs -// AdaptySDK -// - -using System; -using System.Collections.Generic; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyFlowPaywall - { - public partial class ProductReference - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("vendor_product_id", VendorProductId); - node.Add("adapty_product_id", AdaptyProductId); - node.Add("access_level_id", AccessLevelId); - node.Add("product_type", ProductType); - - if (FlowProductId != null) - node.Add("flow_product_id", FlowProductId); - -#if UNITY_ANDROID - if (AndroidBasePlanId != null) - node.Add("base_plan_id", AndroidBasePlanId); - if (AndroidOfferId != null) - node.Add("offer_id", AndroidOfferId); -#endif - -#if UNITY_IOS - if (PromotionalOfferId != null) - node.Add("promotional_offer_id", PromotionalOfferId); - if (WinBackOfferId != null) - node.Add("win_back_offer_id", WinBackOfferId); -#endif - return node; - } - - internal ProductReference(JSONObject jsonNode) - { - VendorProductId = jsonNode.GetString("vendor_product_id"); - AdaptyProductId = jsonNode.GetString("adapty_product_id"); - AccessLevelId = jsonNode.GetString("access_level_id"); - ProductType = jsonNode.GetString("product_type"); - FlowProductId = jsonNode.GetStringIfPresent("flow_product_id"); - -#if UNITY_ANDROID - AndroidBasePlanId = jsonNode.GetStringIfPresent("base_plan_id"); - AndroidOfferId = jsonNode.GetStringIfPresent("offer_id"); -#else - AndroidBasePlanId = null; - AndroidOfferId = null; -#endif - -#if UNITY_IOS - PromotionalOfferId = jsonNode.GetStringIfPresent("promotional_offer_id"); - WinBackOfferId = jsonNode.GetStringIfPresent("win_back_offer_id"); -#else - PromotionalOfferId = null; - WinBackOfferId = null; -#endif - } - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyFlowPaywall.ProductReference GetAdaptyFlowPaywallProductReference( - this JSONNode node, - string aKey - ) => new AdaptyFlowPaywall.ProductReference(GetObject(node, aKey)); - - internal static AdaptyFlowPaywall.ProductReference GetAdaptyFlowPaywallProductReferenceIfPresent( - this JSONNode node, - string aKey - ) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) - return null; - return new AdaptyFlowPaywall.ProductReference(obj); - } - - internal static IList GetAdaptyFlowPaywallProductReferenceList( - this JSONNode node, - string aKey - ) - { - var array = GetArray(node, aKey); - var result = new List(); - foreach (var item in array.Children) - { - if (!item.IsObject) - throw new Exception($"Value by index: {result.Count} is not Object"); - result.Add(new AdaptyFlowPaywall.ProductReference(item.AsObject)); - } - return result; - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall.ProductReference+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall.ProductReference+JSON.cs.meta deleted file mode 100644 index ed234cf..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyFlowPaywall.ProductReference+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: ff6675879bdae4fa5a6d79f6bfd94058 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationDetails+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationDetails+JSON.cs deleted file mode 100644 index f305048..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationDetails+JSON.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -// AdaptyInstallationDetails+JSON.cs -// AdaptySDK -// -// Created by Alexey Goncharov on 10.09.2025. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyInstallationDetails - { - internal AdaptyInstallationDetails(JSONObject jsonNode) - { - InstallId = jsonNode.GetStringIfPresent("install_id"); - InstallTime = jsonNode.GetDateTime("install_time"); - AppLaunchCount = jsonNode.GetInteger("app_launch_count"); - Payload = jsonNode.GetStringIfPresent("payload"); - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyInstallationDetails GetAdaptyInstallationDetails( - this JSONNode node - ) => new AdaptyInstallationDetails(JSONNodeExtensions.GetObject(node)); - - internal static AdaptyInstallationDetails GetAdaptyInstallationDetails( - this JSONNode node, - string aKey - ) => new AdaptyInstallationDetails(JSONNodeExtensions.GetObject(node, aKey)); - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationDetails+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationDetails+JSON.cs.meta deleted file mode 100644 index 477ff0b..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationDetails+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: faba425e706e849c5b83da84847c5a33 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationStatus+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationStatus+JSON.cs deleted file mode 100644 index c90cce8..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationStatus+JSON.cs +++ /dev/null @@ -1,57 +0,0 @@ -// -// AdaptyInstallationStatus+JSON.cs -// AdaptySDK -// -// Created by Alexey Goncharov on 10.09.2025. -// - -using System; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - internal static partial class AdaptyInstallationStatusFactory - { - internal static AdaptyInstallationStatus CreateFromJSON(JSONObject jsonNode) - { - var statusString = jsonNode.GetString("status"); - switch (statusString) - { - case "determined": - var detailsObj = JSONNodeExtensions.GetObjectIfPresent(jsonNode, "details"); - if (detailsObj == null) - { - throw new Exception( - "AdaptyInstallationStatus 'determined' requires 'details' field" - ); - } - return new AdaptyInstallationStatusDetermined( - new AdaptyInstallationDetails(detailsObj) - ); - case "not_available": - return new AdaptyInstallationStatusNotAvailable(); - case "not_determined": - return new AdaptyInstallationStatusNotDetermined(); - default: - throw new Exception( - $"Unknown AdaptyInstallationStatus status: '{statusString}'" - ); - } - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyInstallationStatus GetInstallationStatus(this JSONNode node) => - AdaptyInstallationStatusFactory.CreateFromJSON(JSONNodeExtensions.GetObject(node)); - - internal static AdaptyInstallationStatus GetInstallationStatus( - this JSONNode node, - string aKey - ) => AdaptyInstallationStatusFactory.CreateFromJSON(JSONNodeExtensions.GetObject(node, aKey)); - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationStatus+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationStatus+JSON.cs.meta deleted file mode 100644 index 4d8823e..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyInstallationStatus+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f33ddab420d1c4a53bdb522abefc8844 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyLogLevel+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyLogLevel+JSON.cs deleted file mode 100644 index e752ed1..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyLogLevel+JSON.cs +++ /dev/null @@ -1,43 +0,0 @@ -// -// AdaptyLogLevel+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static JSONNode ToJSONNode(this AdaptyLogLevel value) => - value switch - { - AdaptyLogLevel.Error => "error", - AdaptyLogLevel.Warn => "warn", - AdaptyLogLevel.Info => "info", - AdaptyLogLevel.Verbose => "verbose", - AdaptyLogLevel.Debug => "debug", - _ => throw new Exception($"AdaptyLog.Level unknown value: {value}"), - }; - - internal static AdaptyLogLevel GetAdaptyLogLevel(this JSONNode node) => - GetString(node).ToAdaptyLogLevel(); - internal static AdaptyLogLevel GetAdaptyLogLevel(this JSONNode node, string aKey) => - GetString(node, aKey).ToAdaptyLogLevel(); - internal static AdaptyLogLevel? GetAdaptyLogLevelIfPresent(this JSONNode node, string aKey) => - GetStringIfPresent(node, aKey)?.ToAdaptyLogLevel(); - - private static AdaptyLogLevel ToAdaptyLogLevel(this string value) => - value switch - { - "error" => AdaptyLogLevel.Error, - "warn" => AdaptyLogLevel.Warn, - "info" => AdaptyLogLevel.Info, - "verbose" => AdaptyLogLevel.Verbose, - "debug" => AdaptyLogLevel.Debug, - _ => throw new Exception($"AdaptyLog.Level unknown value: {value}"), - }; - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboarding+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboarding+JSON.cs deleted file mode 100644 index e8a747d..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboarding+JSON.cs +++ /dev/null @@ -1,65 +0,0 @@ -// -// AdaptyOnboarding+JSON.cs -// AdaptySDK -// -// Created by Alexey Goncharov on 10.09.2025. -// - -using System; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyOnboarding - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("placement", Placement.ToJSONNode()); - node.Add("onboarding_id", OnboardingId); - node.Add("onboarding_name", Name); - node.Add("variation_id", VariationId); - if (RemoteConfig != null) - node.Add("remote_config", RemoteConfig.ToJSONNode()); - - var builder = new JSONObject(); - builder.Add("config_url", _Builder.ConfigUrl); - node.Add("onboarding_builder", builder); - - if (_PayloadData != null) - node.Add("payload_data", _PayloadData); - node.Add("response_created_at", _ResponseCreatedAt); - node.Add("request_locale", _RequestLocale); - return node; - } - - internal AdaptyOnboarding(JSONObject jsonNode) - { - Placement = jsonNode.GetPlacement("placement"); - OnboardingId = jsonNode.GetString("onboarding_id"); - Name = jsonNode.GetString("onboarding_name"); - VariationId = jsonNode.GetString("variation_id"); - RemoteConfig = jsonNode.GetRemoteConfigIfPresent("remote_config"); - - var builder = JSONNodeExtensions.GetObject(jsonNode, "onboarding_builder"); - _Builder = new OnboardingBuilder(builder.GetString("config_url")); - - _PayloadData = jsonNode.GetStringIfPresent("payload_data"); - _ResponseCreatedAt = jsonNode.GetLong("response_created_at"); - _RequestLocale = jsonNode.GetString("request_locale"); - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyOnboarding GetOnboarding(this JSONNode node) => - new AdaptyOnboarding(JSONNodeExtensions.GetObject(node)); - - internal static AdaptyOnboarding GetOnboarding(this JSONNode node, string aKey) => - new AdaptyOnboarding(JSONNodeExtensions.GetObject(node, aKey)); - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboarding+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboarding+JSON.cs.meta deleted file mode 100644 index 173f7af..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboarding+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 16105a5e33ecf4522a2278894041d42f \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsAnalyticsEvent+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsAnalyticsEvent+JSON.cs deleted file mode 100644 index 3c14cfb..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsAnalyticsEvent+JSON.cs +++ /dev/null @@ -1,45 +0,0 @@ -// -// AdaptyOnboardingsAnalyticsEvent+JSON.cs -// AdaptySDK -// -// Created by GPT-5 on 17.09.2025. -// - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptySDK.AdaptyOnboardingsAnalyticsEvent GetOnboardingsAnalyticsEvent( - this JSONNode node, - string aKey - ) - { - var obj = JSONNodeExtensions.GetObject(node, aKey); - var name = obj.GetString("name"); - switch (name) - { - case "onboarding_started": - return new AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingStarted(); - case "screen_presented": - return new AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenPresented(); - case "screen_completed": - return new AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted( - obj.GetStringIfPresent("element_id"), - obj.GetStringIfPresent("reply") - ); - case "second_screen_presented": - return new AdaptySDK.AdaptyOnboardingsAnalyticsEventSecondScreenPresented(); - case "registration_screen_presented": - return new AdaptySDK.AdaptyOnboardingsAnalyticsEventRegistrationScreenPresented(); - case "products_screen_presented": - return new AdaptySDK.AdaptyOnboardingsAnalyticsEventProductsScreenPresented(); - case "user_email_collected": - return new AdaptySDK.AdaptyOnboardingsAnalyticsEventUserEmailCollected(); - case "onboarding_completed": - return new AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingCompleted(); - default: - return new AdaptySDK.AdaptyOnboardingsAnalyticsEventUnknown(name); - } - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsAnalyticsEvent+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsAnalyticsEvent+JSON.cs.meta deleted file mode 100644 index 7361bb2..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsAnalyticsEvent+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 22448087943304eb788994dd2ff8cea7 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsStateUpdatedParams+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsStateUpdatedParams+JSON.cs deleted file mode 100644 index 421ef6b..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsStateUpdatedParams+JSON.cs +++ /dev/null @@ -1,86 +0,0 @@ -// -// AdaptyOnboardingsStateUpdatedParams+JSON.cs -// AdaptySDK -// -// Created by GPT-5 on 17.09.2025. -// - -using System.Collections.Generic; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - // action: { element_id, element_type, value } - internal static AdaptySDK.AdaptyOnboardingsStateUpdatedParams GetOnboardingsStateUpdatedParams( - this JSONNode node, - string aKey - ) - { - var actionObj = JSONNodeExtensions.GetObject(node, aKey); - var elementType = actionObj.GetString("element_type"); - switch (elementType) - { - case "select": - { - var valueObj = JSONNodeExtensions.GetObject(actionObj, "value"); - var id = valueObj.GetString("id"); - var value = valueObj.GetString("value"); - var label = valueObj.GetString("label"); - return new AdaptySDK.AdaptyOnboardingsSelectParams(id, value, label); - } - case "multi_select": - { - var array = JSONNodeExtensions.GetArray(actionObj, "value"); - var list = new List(array.Count); - foreach (var item in array.Children) - { - var id = item.GetString("id"); - var value = item.GetString("value"); - var label = item.GetString("label"); - list.Add(new AdaptySDK.AdaptyOnboardingsSelectParams(id, value, label)); - } - return new AdaptySDK.AdaptyOnboardingsMultiSelectParams(list); - } - case "input": - { - var valueObj = JSONNodeExtensions.GetObject(actionObj, "value"); - var type = valueObj.GetString("type"); - switch (type) - { - case "text": - return new AdaptySDK.AdaptyOnboardingsInputParams( - new AdaptySDK.AdaptyOnboardingsTextInput( - valueObj.GetString("value") - ) - ); - case "email": - return new AdaptySDK.AdaptyOnboardingsInputParams( - new AdaptySDK.AdaptyOnboardingsEmailInput( - valueObj.GetString("value") - ) - ); - case "number": - return new AdaptySDK.AdaptyOnboardingsInputParams( - new AdaptySDK.AdaptyOnboardingsNumberInput( - valueObj.GetDouble("value") - ) - ); - default: - return null; - } - } - case "date_picker": - { - var valueObj = JSONNodeExtensions.GetObject(actionObj, "value"); - int? day = valueObj.GetIntegerIfPresent("day"); - int? month = valueObj.GetIntegerIfPresent("month"); - int? year = valueObj.GetIntegerIfPresent("year"); - return new AdaptySDK.AdaptyOnboardingsDatePickerParams(day, month, year); - } - default: - return null; - } - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsStateUpdatedParams+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsStateUpdatedParams+JSON.cs.meta deleted file mode 100644 index 424893f..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyOnboardingsStateUpdatedParams+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 86baba8343ee04299867673ea7b799c1 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaymentMode+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaymentMode+JSON.cs deleted file mode 100644 index 70ee965..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaymentMode+JSON.cs +++ /dev/null @@ -1,29 +0,0 @@ -// -// AdaptyPaymentMode+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyPaymentMode GetAdaptyPaymentMode(this JSONNode node, string aKey) - => GetString(node, aKey).ToAdaptyPaymentMode(); - - internal static AdaptyPaymentMode? GetAdaptyPaymentModeIfPresent(this JSONNode node, string aKey) - => GetStringIfPresent(node, aKey)?.ToAdaptyPaymentMode(); - - private static AdaptyPaymentMode ToAdaptyPaymentMode(this string value) - { - switch (value) - { - case "pay_as_you_go": return AdaptyPaymentMode.PayAsYouGo; - case "pay_up_front": return AdaptyPaymentMode.PayUpFront; - case "free_trial": return AdaptyPaymentMode.FreeTrial; - default: return AdaptyPaymentMode.Unknown; - } - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaywallProduct+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaywallProduct+JSON.cs deleted file mode 100644 index f98230b..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaywallProduct+JSON.cs +++ /dev/null @@ -1,131 +0,0 @@ -// -// AdaptyPaywallProduct+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System; -using System.Collections.Generic; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyPaywallProduct - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("vendor_product_id", VendorProductId); - node.Add("adapty_product_id", AdaptyProductId); - node.Add("access_level_id", AccessLevelId); - node.Add("product_type", ProductType); - node.Add("paywall_variation_id", PaywallVariationId); - node.Add("paywall_ab_test_name", PaywallABTestName); - node.Add("paywall_name", PaywallName); - node.Add("paywall_product_index", PaywallProductIndex); - - if (_WebPurchaseUrl != null) - { - node.Add("web_purchase_url", _WebPurchaseUrl); - } - - if (_PayloadData != null) - { - node.Add("payload_data", _PayloadData); - } - - var offer = Subscription?.Offer; - if (offer != null) - { - var subNode = new JSONObject(); - if (offer.Identifier != null) - { - subNode.Add("id", offer.Identifier); - } - - subNode.Add("type", offer.Type.ToJSONNode()); - node.Add("subscription_offer_identifier", subNode); - } - - return node; - } - - internal AdaptyPaywallProduct(JSONObject jsonNode) - { - VendorProductId = jsonNode.GetString("vendor_product_id"); - AdaptyProductId = jsonNode.GetString("adapty_product_id"); - FlowProductId = jsonNode.GetStringIfPresent("flow_product_id"); - AccessLevelId = jsonNode.GetString("access_level_id"); - ProductType = jsonNode.GetString("product_type"); - PaywallVariationId = jsonNode.GetString("paywall_variation_id"); - PaywallABTestName = jsonNode.GetString("paywall_ab_test_name"); - PaywallName = jsonNode.GetString("paywall_name"); - LocalizedDescription = jsonNode.GetString("localized_description"); - LocalizedTitle = jsonNode.GetString("localized_title"); -#if UNITY_IOS - IsFamilyShareable = jsonNode.GetBoolean("is_family_shareable"); -#else - IsFamilyShareable = false; -#endif - RegionCode = jsonNode.GetStringIfPresent("region_code"); - Price = jsonNode.GetAdaptyPrice("price"); - Subscription = jsonNode.GetAdaptySubscriptionIfPresent("subscription"); - PaywallProductIndex = jsonNode.GetInteger("paywall_product_index"); - _PayloadData = jsonNode.GetStringIfPresent("payload_data"); - _WebPurchaseUrl = jsonNode.GetStringIfPresent("web_purchase_url"); - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyPaywallProduct GetAdaptyPaywallProduct( - this JSONNode node, - string aKey - ) => new AdaptyPaywallProduct(GetObject(node, aKey)); - - internal static AdaptyPaywallProduct GetAdaptyPaywallProductIfPresent( - this JSONNode node, - string aKey - ) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) - return null; - return new AdaptyPaywallProduct(obj); - } - - internal static IList GetAdaptyPaywallProductList(this JSONNode node) - { - var array = GetArrayIfPresent(node); - return GetAdaptyPaywallProductList(array); - } - - internal static IList GetAdaptyPaywallProductList( - this JSONNode node, - string aKey - ) - { - var array = GetArrayIfPresent(node, aKey); - return GetAdaptyPaywallProductList(array); - } - - private static IList GetAdaptyPaywallProductList(this JSONArray array) - { - if (array is null) - return null; - var result = new List(); - foreach (var item in array.Children) - { - if (!item.IsObject) - throw new Exception($"Value by index: {result.Count} is not Object"); - result.Add(new AdaptyPaywallProduct(item.AsObject)); - } - return result; - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaywallProduct+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaywallProduct+JSON.cs.meta deleted file mode 100644 index 912f726..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPaywallProduct+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 62fe8d9753ddb41ba82607b678deb376 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacement+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacement+JSON.cs deleted file mode 100644 index c96466e..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacement+JSON.cs +++ /dev/null @@ -1,52 +0,0 @@ -// -// AdaptyPlacement+JSON.cs -// AdaptySDK -// -// Created by Aleksei Goncharov on 09.09.2025. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyPlacement - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("developer_id", Id); - node.Add("audience_name", AudienceName); - node.Add("revision", Revision); - node.Add("ab_test_name", ABTestName); - node.Add("placement_audience_version_id", PlacementAudienceVersionId); - - if (IsTrackingPurchases != null) - { - node.Add("is_tracking_purchases", IsTrackingPurchases); - } - return node; - } - - internal AdaptyPlacement(JSONObject jsonNode) - { - Id = jsonNode.GetString("developer_id"); - AudienceName = jsonNode.GetString("audience_name"); - Revision = jsonNode.GetLong("revision"); - ABTestName = jsonNode.GetString("ab_test_name"); - PlacementAudienceVersionId = jsonNode.GetString("placement_audience_version_id"); - IsTrackingPurchases = jsonNode.GetBooleanIfPresent("is_tracking_purchases") ?? false; - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyPlacement GetPlacement(this JSONNode node) => - new AdaptyPlacement(GetObject(node)); - - internal static AdaptyPlacement GetPlacement(this JSONNode node, string aKey) => - new AdaptyPlacement(GetObject(node, aKey)); - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacement+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacement+JSON.cs.meta deleted file mode 100644 index 497009f..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacement+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: af198ec0494674c7bb61c35af28270f6 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacementFetchPolicy+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacementFetchPolicy+JSON.cs deleted file mode 100644 index 8bd3a4b..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacementFetchPolicy+JSON.cs +++ /dev/null @@ -1,29 +0,0 @@ -// -// AdaptyPlacementFetchPolicy+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 26.12.2023. -// - -namespace AdaptySDK -{ - using System; - using AdaptySDK.SimpleJSON; - - public partial class AdaptyPlacementFetchPolicy - { - internal JSONNode ToJSONNode() - { - double? maxAgeInSeconds = _MaxAge.HasValue ? _MaxAge.Value.TotalSeconds : null; - - var node = new JSONObject(); - node.Add("type", _Type); - if (maxAgeInSeconds != null) - { - node.Add("max_age", maxAgeInSeconds); - } - - return node; - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacementFetchPolicy+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacementFetchPolicy+JSON.cs.meta deleted file mode 100644 index e592690..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPlacementFetchPolicy+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 81ab0e4a8c40e4cb88891abfacd3c81f \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPrice+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPrice+JSON.cs deleted file mode 100644 index f56e63c..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPrice+JSON.cs +++ /dev/null @@ -1,39 +0,0 @@ -// -// AdaptyPrice+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using AdaptySDK.SimpleJSON; - -namespace AdaptySDK -{ - public partial class AdaptyPrice - { - internal AdaptyPrice(JSONObject jsonNode) - { - Amount = jsonNode.GetDouble("amount"); - CurrencyCode = jsonNode.GetStringIfPresent("currency_code"); - CurrencySymbol = jsonNode.GetStringIfPresent("currency_symbol"); - LocalizedString = jsonNode.GetStringIfPresent("localized_string"); - } - - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyPrice GetAdaptyPrice(this JSONNode node, string aKey) => - new AdaptyPrice(GetObject(node, aKey)); - - internal static AdaptyPrice GetAdaptyPriceIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) return null; - return new AdaptyPrice(obj); - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPrice+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPrice+JSON.cs.meta deleted file mode 100644 index da244b6..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPrice+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b99e3c52cb62147ef9986689cd8e68f5 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProductIdentifier+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProductIdentifier+JSON.cs deleted file mode 100644 index 847626b..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProductIdentifier+JSON.cs +++ /dev/null @@ -1,35 +0,0 @@ -// -// AdaptyProductIdentifier+JSON.cs -// AdaptySDK -// -// Created by Alexey Goncharov on 10.09.2025. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyProductIdentifier - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("vendor_product_id", VendorProductId); - node.Add("adapty_product_id", _AdaptyProductId); - - if (!string.IsNullOrEmpty(BasePlanId)) - { - node.Add("base_plan_id", BasePlanId); - } - - return node; - } - - internal AdaptyProductIdentifier(JSONObject jsonNode) - { - VendorProductId = jsonNode.GetString("vendor_product_id"); - _AdaptyProductId = jsonNode.GetString("adapty_product_id"); - BasePlanId = jsonNode.GetStringIfPresent("base_plan_id"); - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProductIdentifier+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProductIdentifier+JSON.cs.meta deleted file mode 100644 index f5443f9..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProductIdentifier+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c7ffb707c9fba4d7fa4ebb188a875bb1 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile+JSON.cs deleted file mode 100644 index 15427fd..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile+JSON.cs +++ /dev/null @@ -1,60 +0,0 @@ -// -// AdaptyProfile+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System.Collections.Generic; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyProfile - { - internal AdaptyProfile(JSONObject jsonNode) - { - ProfileId = jsonNode.GetString("profile_id"); - SegmentId = jsonNode.GetString("segment_hash"); - CustomerUserId = jsonNode.GetStringIfPresent("customer_user_id"); - AppliedAttributionSources = - jsonNode.GetStringListIfPresent("applied_attribution_sources") - ?? new List(); - CustomAttributes = - jsonNode.GetDictionaryIfPresent("custom_attributes") - ?? new Dictionary(); - AccessLevels = - jsonNode.GetAccessLevelDictionaryIfPresent("paid_access_levels") - ?? new Dictionary(); - Subscriptions = - jsonNode.GetSubscriptionDictionaryIfPresent("subscriptions") - ?? new Dictionary(); - NonSubscriptions = - jsonNode.GetNonSubscriptionDictionaryIfPresent("non_subscriptions") - ?? new Dictionary>(); - Version = jsonNode.GetLong("timestamp"); - IsTestUser = jsonNode.GetBoolean("is_test_user"); - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyProfile GetAdaptyProfile(this JSONNode node) => - new AdaptyProfile(GetObject(node)); - - internal static AdaptyProfile GetAdaptyProfile(this JSONNode node, string aKey) => - new AdaptyProfile(GetObject(node, aKey)); - - internal static AdaptyProfile GetAdaptyProfileIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) - return null; - return new AdaptyProfile(obj); - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile+JSON.cs.meta deleted file mode 100644 index bc8e690..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 03c3242fb75564769a34eeb994829985 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.AccessLevel+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.AccessLevel+JSON.cs deleted file mode 100644 index d1dbb01..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.AccessLevel+JSON.cs +++ /dev/null @@ -1,72 +0,0 @@ -// -// AdaptyProfile.AccessLevel+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// -using System; -using System.Collections.Generic; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyProfile - { - public partial class AccessLevel - { - internal AccessLevel(JSONObject jsonNode) - { - Id = jsonNode.GetString("id"); - IsActive = jsonNode.GetBoolean("is_active"); - VendorProductId = jsonNode.GetString("vendor_product_id"); - Store = jsonNode.GetString("store"); - ActivatedAt = jsonNode.GetDateTime("activated_at"); - RenewedAt = jsonNode.GetDateTimeIfPresent("renewed_at"); - ExpiresAt = jsonNode.GetDateTimeIfPresent("expires_at"); - IsLifetime = jsonNode.GetBoolean("is_lifetime"); - ActiveIntroductoryOfferType = jsonNode.GetStringIfPresent("active_introductory_offer_type"); - ActivePromotionalOfferType = jsonNode.GetStringIfPresent("active_promotional_offer_type"); - ActivePromotionalOfferId = jsonNode.GetStringIfPresent("active_promotional_offer_id"); - OfferId = jsonNode.GetStringIfPresent("offer_id"); - WillRenew = jsonNode.GetBoolean("will_renew"); - IsInGracePeriod = jsonNode.GetBoolean("is_in_grace_period"); - UnsubscribedAt = jsonNode.GetDateTimeIfPresent("unsubscribed_at"); - BillingIssueDetectedAt = jsonNode.GetDateTimeIfPresent("billing_issue_detected_at"); - StartsAt = jsonNode.GetDateTimeIfPresent("starts_at"); - CancellationReason = jsonNode.GetStringIfPresent("cancellation_reason"); - IsRefund = jsonNode.GetBoolean("is_refund"); - } - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyProfile.AccessLevel GetAccessLevel(this JSONNode node, string aKey) - => new AdaptyProfile.AccessLevel(GetObject(node, aKey)); - - internal static AdaptyProfile.AccessLevel GetAccessLevelIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) return null; - return new AdaptyProfile.AccessLevel(obj); - } - - internal static IDictionary GetAccessLevelDictionaryIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj == null) return null; - var result = new Dictionary(); - foreach (var item in obj) - { - var value = item.Value; - if (!value.IsObject) throw new Exception($"Value by key: {item.Key} is not Object"); - result.Add(item.Key, new AdaptyProfile.AccessLevel(value.AsObject)); - } - return result; - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.AccessLevel+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.AccessLevel+JSON.cs.meta deleted file mode 100644 index 0449761..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.AccessLevel+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: ca72425a88fae42e08e61dae36c2dda9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.NonSubscription+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.NonSubscription+JSON.cs deleted file mode 100644 index dd87764..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.NonSubscription+JSON.cs +++ /dev/null @@ -1,68 +0,0 @@ -// -// AdaptyProfile.NonSubscription+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System; -using System.Collections.Generic; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyProfile - { - public partial class NonSubscription - { - internal NonSubscription(JSONObject jsonNode) - { - PurchaseId = jsonNode.GetString("purchase_id"); - Store = jsonNode.GetString("store"); - VendorProductId = jsonNode.GetString("vendor_product_id"); - VendorTransactionId = jsonNode.GetStringIfPresent("vendor_transaction_id"); - PurchasedAt = jsonNode.GetDateTime("purchased_at"); - IsConsumable = jsonNode.GetBoolean("is_consumable"); - IsSandbox = jsonNode.GetBoolean("is_sandbox"); - IsRefund = jsonNode.GetBoolean("is_refund"); - } - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyProfile.NonSubscription GetNonSubscription(this JSONNode node, string aKey) - => new AdaptyProfile.NonSubscription(GetObject(node, aKey)); - - internal static AdaptyProfile.NonSubscription GetNonSubscriptionIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) return null; - return new AdaptyProfile.NonSubscription(obj); - } - - internal static IDictionary> GetNonSubscriptionDictionaryIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj == null) return null; - var result = new Dictionary>(); - foreach (var item in obj) - { - var array = item.Value; - if (!array.IsArray) throw new Exception($"Value by key: {item.Key} is not Array"); - var list = new List(); - foreach (var value in array.Children) - { - if (!value.IsObject) throw new Exception($"Value by index: {result.Count} is not Object"); - list.Add(new AdaptyProfile.NonSubscription(value.AsObject)); - } - result.Add(item.Key, list); - } - return result; - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.NonSubscription+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.NonSubscription+JSON.cs.meta deleted file mode 100644 index cbb8f6b..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.NonSubscription+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 6a5998269d5a44bb58fef884df158c48 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.Subscription+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.Subscription+JSON.cs deleted file mode 100644 index aa2c11d..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.Subscription+JSON.cs +++ /dev/null @@ -1,74 +0,0 @@ -// -// AdaptyProfile.Subscription+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// -using System; -using System.Collections.Generic; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyProfile - { - public partial class Subscription - { - internal Subscription(JSONObject jsonNode) - { - IsActive = jsonNode.GetBoolean("is_active"); - VendorProductId = jsonNode.GetString("vendor_product_id"); - Store = jsonNode.GetString("store"); - ActivatedAt = jsonNode.GetDateTime("activated_at"); - RenewedAt = jsonNode.GetDateTimeIfPresent("renewed_at"); - ExpiresAt = jsonNode.GetDateTimeIfPresent("expires_at"); - StartsAt = jsonNode.GetDateTimeIfPresent("starts_at"); - IsLifetime = jsonNode.GetBoolean("is_lifetime"); - ActiveIntroductoryOfferType = jsonNode.GetStringIfPresent("active_introductory_offer_type"); - ActivePromotionalOfferType = jsonNode.GetStringIfPresent("active_promotional_offer_type"); - ActivePromotionalOfferId = jsonNode.GetStringIfPresent("active_promotional_offer_id"); - OfferId = jsonNode.GetStringIfPresent("offer_id"); - WillRenew = jsonNode.GetBoolean("will_renew"); - IsInGracePeriod = jsonNode.GetBoolean("is_in_grace_period"); - UnsubscribedAt = jsonNode.GetDateTimeIfPresent("unsubscribed_at"); - BillingIssueDetectedAt = jsonNode.GetDateTimeIfPresent("billing_issue_detected_at"); - IsSandbox = jsonNode.GetBoolean("is_sandbox"); - VendorTransactionId = jsonNode.GetString("vendor_transaction_id"); - VendorOriginalTransactionId = jsonNode.GetString("vendor_original_transaction_id"); - CancellationReason = jsonNode.GetStringIfPresent("cancellation_reason"); - IsRefund = jsonNode.GetBoolean("is_refund"); - } - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyProfile.Subscription GetSubscription(this JSONNode node, string aKey) - => new AdaptyProfile.Subscription(GetObject(node, aKey)); - - internal static AdaptyProfile.Subscription GetSubscriptionIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) return null; - return new AdaptyProfile.Subscription(obj); - } - - internal static IDictionary GetSubscriptionDictionaryIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj == null) return null; - var result = new Dictionary(); - foreach (var item in obj) - { - var value = item.Value; - if (!value.IsObject) throw new Exception($"Value by key: {item.Key} is not Object"); - result.Add(item.Key, new AdaptyProfile.Subscription(value.AsObject)); - } - return result; - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.Subscription+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.Subscription+JSON.cs.meta deleted file mode 100644 index fb28793..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfile.Subscription+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 37b425e3a6a9e4362a4d240454904fc6 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileGender+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileGender+JSON.cs deleted file mode 100644 index b4c4264..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileGender+JSON.cs +++ /dev/null @@ -1,38 +0,0 @@ -// -// AdaptyProfileGender.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static JSONNode ToJSONNode(this AdaptyProfileGender value) - { - switch (value) - { - case AdaptyProfileGender.Female: return "f"; - case AdaptyProfileGender.Male: return "m"; - default: return "o"; - } - } - - internal static AdaptyProfileGender GetProfileGender(this JSONNode node, string aKey) - => GetString(node, aKey).ToProfileGender(); - - internal static AdaptyProfileGender? GetProfileGenderIfPresent(this JSONNode node, string aKey) - => GetStringIfPresent(node, aKey)?.ToProfileGender(); - - private static AdaptyProfileGender ToProfileGender(this string value) - { - switch (value) - { - case "f": return AdaptyProfileGender.Female; - case "m": return AdaptyProfileGender.Male; - default: return AdaptyProfileGender.Other; - } - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileGender+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileGender+JSON.cs.meta deleted file mode 100644 index 6618e1b..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileGender+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 5b791108095644abc9295fd3428e551a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileParameters+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileParameters+JSON.cs deleted file mode 100644 index 51e1059..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileParameters+JSON.cs +++ /dev/null @@ -1,32 +0,0 @@ -// -// AdaptyProfileParameters+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - public partial class AdaptyProfileParameters - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - - if (FirstName != null) node.Add("first_name", FirstName); - if (LastName != null) node.Add("last_name", LastName); - if (Gender.HasValue) node.Add("gender", Gender.Value.ToJSONNode()); - if (Birthday != null) node.Add("birthday", $"{Birthday.Value.Year}-{Birthday.Value.Month}-{Birthday.Value.Day}"); - if (Email != null) node.Add("email", Email); - if (PhoneNumber != null) node.Add("phone_number", PhoneNumber); -#if UNITY_IOS - if (AppTrackingTransparencyStatus != null) node.Add("att_status", AppTrackingTransparencyStatus.Value.ToJSON()); -#endif - if (AnalyticsDisabled != null) node.Add("analytics_disabled", AnalyticsDisabled.Value); - if (CustomAttributes.Count > 0) node.Add("custom_attributes", CustomAttributes.ToJSONObject()); - - return node; - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileParameters+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileParameters+JSON.cs.meta deleted file mode 100644 index 1589db7..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyProfileParameters+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a7df7ff6c194d4b0fb45a49102fe96bc -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseParameters+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseParameters+JSON.cs deleted file mode 100644 index 23fa5f3..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseParameters+JSON.cs +++ /dev/null @@ -1,40 +0,0 @@ -// -// AdaptyPurchaseParameters+JSON.cs -// AdaptySDK -// -// Created by Alexey Goncharov on 10.09.2025. -// - -using System; -using System.Collections.Generic; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyPurchaseParameters - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - - if (SubscriptionUpdateParams != null) - { - node.Add(_Keys.SubscriptionUpdateParams, SubscriptionUpdateParams.ToJSONNode()); - } - - if (IsOfferPersonalized.HasValue) - { - node.Add(_Keys.IsOfferPersonalized, IsOfferPersonalized.Value); - } - - return node; - } - } - - internal static class _Keys - { - internal const string SubscriptionUpdateParams = "subscription_update_params"; - internal const string IsOfferPersonalized = "is_offer_personalized"; - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseParameters+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseParameters+JSON.cs.meta deleted file mode 100644 index fb0c251..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseParameters+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 136094caa34964c2ca8830cb43b80195 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResult+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResult+JSON.cs deleted file mode 100644 index fa6b0a4..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResult+JSON.cs +++ /dev/null @@ -1,50 +0,0 @@ -// -// AdaptyPurchaseResult+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 17.12.2024. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyPurchaseResult - { - internal AdaptyPurchaseResult(JSONObject jsonNode) - { - Type = jsonNode.GetAdaptyPurchaseResultType("type"); - Profile = jsonNode.GetAdaptyProfileIfPresent("profile"); - AppleJWSTransaction = jsonNode.GetStringIfPresent("apple_jws_transaction"); - GooglePurchaseToken = jsonNode.GetStringIfPresent("google_purchase_token"); - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyPurchaseResult GetAdaptyPurchaseResult(this JSONNode node) => - new AdaptyPurchaseResult(GetObject(node)); - - internal static AdaptyPurchaseResult GetAdaptyPurchaseResult( - this JSONNode node, - string aKey - ) => new AdaptyPurchaseResult(GetObject(node, aKey)); - - internal static AdaptyPurchaseResult GetAdaptyPurchaseResultIfPresent( - this JSONNode node, - string aKey - ) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) - { - return null; - } - - return new AdaptyPurchaseResult(obj); - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResult+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResult+JSON.cs.meta deleted file mode 100644 index 3d20d4b..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResult+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9f6f1742a628d443bb4411dd3d747935 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResultType+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResultType+JSON.cs deleted file mode 100644 index c8254e2..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResultType+JSON.cs +++ /dev/null @@ -1,31 +0,0 @@ -// -// AdaptyPurchaseResultType+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 17.12.2024. -// - -using System; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - - internal static AdaptyPurchaseResultType GetAdaptyPurchaseResultType(this JSONNode node) => - GetString(node).ToAdaptyPurchaseResultType(); - internal static AdaptyPurchaseResultType GetAdaptyPurchaseResultType(this JSONNode node, string aKey) => - GetString(node, aKey).ToAdaptyPurchaseResultType(); - internal static AdaptyPurchaseResultType? GetAdaptyPurchaseResultTypeIfPresent(this JSONNode node, string aKey) => - GetStringIfPresent(node, aKey)?.ToAdaptyPurchaseResultType(); - - private static AdaptyPurchaseResultType ToAdaptyPurchaseResultType(this string value) => - value switch - { - "pending" => AdaptyPurchaseResultType.Pending, - "user_cancelled" => AdaptyPurchaseResultType.UserCancelled, - "success" => AdaptyPurchaseResultType.Success, - _ => throw new Exception($"AdaptyPurchaseResultType unknown value: {value}"), - }; - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResultType+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResultType+JSON.cs.meta deleted file mode 100644 index 406051d..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyPurchaseResultType+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 5a6aadb8883704f30bfb9c2aa5a76126 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRefundPreference+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRefundPreference+JSON.cs deleted file mode 100644 index 748a2be..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRefundPreference+JSON.cs +++ /dev/null @@ -1,24 +0,0 @@ -// -// AdaptyRefundPreference+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 19.03.2025. -// - -using System; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static JSONNode ToJSONNode(this AdaptyRefundPreference value) => - value switch - { - AdaptyRefundPreference.NoPreference => "no_preference", - AdaptyRefundPreference.Grant => "grant", - AdaptyRefundPreference.Decline => "decline", - _ => throw new Exception($"AdaptyRefundPreference unknown value: {value}"), - }; - - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRefundPreference+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRefundPreference+JSON.cs.meta deleted file mode 100644 index 94cf444..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRefundPreference+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: cb8afeca70efc4d339ff3309385069ae -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRemoteConfig+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRemoteConfig+JSON.cs deleted file mode 100644 index e6246de..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRemoteConfig+JSON.cs +++ /dev/null @@ -1,65 +0,0 @@ -// -// AdaptyRemoteConfig+JSON.cs -// AdaptySDK -// -// Created by Aleksei Goncharov on 09.09.2025. - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyRemoteConfig - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("lang", Locale); - node.Add("data", Data); - return node; - } - - internal AdaptyRemoteConfig(JSONObject jsonNode) - { - Locale = jsonNode.GetString("lang"); - Data = jsonNode.GetString("data"); - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyRemoteConfig GetRemoteConfig(this JSONNode node) => - new AdaptyRemoteConfig(GetObject(node)); - - internal static AdaptyRemoteConfig GetRemoteConfig(this JSONNode node, string aKey) => - new AdaptyRemoteConfig(GetObject(node, aKey)); - - internal static AdaptyRemoteConfig GetRemoteConfigIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) - return null; - return new AdaptyRemoteConfig(obj); - } - - internal static System.Collections.Generic.IList GetRemoteConfigList( - this JSONNode node, - string aKey - ) - { - var result = new System.Collections.Generic.List(); - var array = GetArrayIfPresent(node, aKey); - if (array is null) - return result; - foreach (var item in array.Children) - { - if (!item.IsObject) - throw new System.Exception($"Value by index: {result.Count} is not Object"); - result.Add(new AdaptyRemoteConfig(item.AsObject)); - } - return result; - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRemoteConfig+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRemoteConfig+JSON.cs.meta deleted file mode 100644 index c7de0ef..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyRemoteConfig+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: b570ecafde08d4d2382f1d71534c0890 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyResult+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyResult+JSON.cs deleted file mode 100644 index 409b58b..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyResult+JSON.cs +++ /dev/null @@ -1,35 +0,0 @@ -// -// AdaptyResult+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyResult GetAdaptyResult(this string json, Func map) - { - AdaptyError error; - var value = default(T); - try - { - var response = JSONNode.Parse(json); - error = response.GetAdaptyErrorIfPresent("error"); - if (error is null) - { - value = map(response.GetJSONNode("success")); - } - } - catch (Exception ex) - { - error = new AdaptyError(AdaptyErrorCode.DecodingFailed, "Failed decoding result ", $"AdaptyUnityError.DecodingFailed({ex})"); - } - - return new AdaptyResult(value, error); - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyResult+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyResult+JSON.cs.meta deleted file mode 100644 index 6dc7051..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyResult+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 43287ba3a66fa49c39e27ba4429fe345 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyServerCluster+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyServerCluster+JSON.cs deleted file mode 100644 index 92308ad..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyServerCluster+JSON.cs +++ /dev/null @@ -1,20 +0,0 @@ -// -// AdaptyServerCluster+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 10.12.2024. -// - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static JSONNode ToJSONNode(this AdaptyServerCluster value) => - value switch - { - AdaptyServerCluster.EU => "eu", - AdaptyServerCluster.CN => "cn", - _ => null, - }; - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyServerCluster+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyServerCluster+JSON.cs.meta deleted file mode 100644 index a3fc19e..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyServerCluster+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 49b1bd56ce01b4a46a0effec9a3cc2b8 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscription+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscription+JSON.cs deleted file mode 100644 index bc966c2..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscription+JSON.cs +++ /dev/null @@ -1,55 +0,0 @@ -// -// AdaptySubscription+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// -using System.Collections.Generic; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptySubscription - { - internal AdaptySubscription(JSONObject jsonNode) - { -#if UNITY_IOS - GroupIdentifier = jsonNode.GetString("group_identifier"); -#else - GroupIdentifier = null; -#endif - Period = jsonNode.GetAdaptySubscriptionPeriod("period"); - LocalizedPeriod = jsonNode.GetStringIfPresent("localized_period"); - Offer = jsonNode.GetAdaptySubscriptionOfferIfPresent("offer"); - -#if UNITY_ANDROID - RenewalType = jsonNode.GetAdaptySubscriptionRenewalType("renewal_type"); - BasePlanId = jsonNode.GetString("base_plan_id"); -#else - RenewalType = AdaptySubscriptionRenewalType.Autorenewable; - BasePlanId = null; -#endif - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptySubscription GetAdaptySubscription(this JSONNode node, string aKey) => - new AdaptySubscription(GetObject(node, aKey)); - - internal static AdaptySubscription GetAdaptySubscriptionIfPresent( - this JSONNode node, - string aKey - ) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) - return null; - return new AdaptySubscription(obj); - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscription+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscription+JSON.cs.meta deleted file mode 100644 index 82e610f..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscription+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a17cbbd2d0b034fb7a47283240bbf24a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOffer+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOffer+JSON.cs deleted file mode 100644 index ec22952..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOffer+JSON.cs +++ /dev/null @@ -1,58 +0,0 @@ -// -// AdaptySubscriptionOffer+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// -using System.Collections.Generic; -using System; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptySubscriptionOffer - { - internal AdaptySubscriptionOffer(JSONObject jsonNode) - { - var subNode = jsonNode.GetObject("offer_identifier"); - Identifier = subNode.GetStringIfPresent("id"); - Type = subNode.GetAdaptySubscriptionOfferType("type"); - Phases = jsonNode.GetAdaptySubscriptionPhaseListIfPresent("phases"); -#if UNITY_ANDROID - OfferTags = jsonNode.GetStringList("offer_tags"); -#else - OfferTags = null; -#endif - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptySubscriptionOffer GetAdaptySubscriptionOffer(this JSONNode node, string aKey) - => new AdaptySubscriptionOffer(GetObject(node, aKey)); - - internal static AdaptySubscriptionOffer GetAdaptySubscriptionOfferIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) return null; - return new AdaptySubscriptionOffer(obj); - } - - internal static IList GetStringList(this JSONNode node, string aKey) - { - var array = GetArrayIfPresent(node, aKey); - if (array is null) return null; - var result = new List(); - foreach (var item in array.Children) - { - if (!item.IsString) throw new Exception($"Value by index: {result.Count} is not String"); - result.Add(item.Value); - } - return result; - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOffer+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOffer+JSON.cs.meta deleted file mode 100644 index 92c2471..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOffer+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 617db0f1022824e018ec63b7ecfe2c21 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOfferType+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOfferType+JSON.cs deleted file mode 100644 index e41f4e2..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOfferType+JSON.cs +++ /dev/null @@ -1,40 +0,0 @@ -// -// AdaptyLogLevel+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static JSONNode ToJSONNode(this AdaptySubscriptionOfferType value) => - value switch - { - AdaptySubscriptionOfferType.Introductory => "introductory", - AdaptySubscriptionOfferType.Promotional => "promotional", - AdaptySubscriptionOfferType.WinBack => "win_back", - AdaptySubscriptionOfferType.Code => "code", - _ => "unknown", - }; - - internal static AdaptySubscriptionOfferType GetAdaptySubscriptionOfferType(this JSONNode node, string aKey) => - GetString(node, aKey).ToAdaptySubscriptionOfferType(); - - internal static AdaptySubscriptionOfferType? GetdaptySubscriptionOfferTypeIfPresent(this JSONNode node, string aKey) => - GetStringIfPresent(node, aKey)?.ToAdaptySubscriptionOfferType(); - - private static AdaptySubscriptionOfferType ToAdaptySubscriptionOfferType(this string value) => - value switch - { - "introductory" => AdaptySubscriptionOfferType.Introductory, - "promotional" => AdaptySubscriptionOfferType.Promotional, - "win_back" => AdaptySubscriptionOfferType.WinBack, - "code" => AdaptySubscriptionOfferType.Code, - _ => throw new Exception($"AdaptySubscriptionOfferType unknown value: {value}"), - }; - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOfferType+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOfferType+JSON.cs.meta deleted file mode 100644 index 70112b6..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionOfferType+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 6848d3aea0b324eed8aad7530dbb8b25 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriod+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriod+JSON.cs deleted file mode 100644 index 2e2263a..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriod+JSON.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -// AdaptySubscriptionPeriod+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptySubscriptionPeriod - { - internal AdaptySubscriptionPeriod(JSONObject jsonNode) - { - Unit = jsonNode.GetAdaptySubscriptionPeriodUnit("unit"); - NumberOfUnits = jsonNode.GetInteger("number_of_units"); - } - } - -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptySubscriptionPeriod GetAdaptySubscriptionPeriod(this JSONNode node, string aKey) - => new AdaptySubscriptionPeriod(GetObject(node, aKey)); - - internal static AdaptySubscriptionPeriod GetAdaptySubscriptionPeriodIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) return null; - return new AdaptySubscriptionPeriod(obj); - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriod+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriod+JSON.cs.meta deleted file mode 100644 index b55e744..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriod+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c7abf25f39a3f4b5f97dfb15975872ff -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriodUnit+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriodUnit+JSON.cs deleted file mode 100644 index efb3c9e..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriodUnit+JSON.cs +++ /dev/null @@ -1,30 +0,0 @@ -// -// AdaptySubscriptionPeriodUnit+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptySubscriptionPeriodUnit GetAdaptySubscriptionPeriodUnit(this JSONNode node, string aKey) - => GetString(node, aKey).ToAdaptySubscriptionPeriodUnit(); - - internal static AdaptySubscriptionPeriodUnit? GetAdaptySubscriptionPeriodUnitIfPresent(this JSONNode node, string aKey) - => GetStringIfPresent(node, aKey)?.ToAdaptySubscriptionPeriodUnit(); - - private static AdaptySubscriptionPeriodUnit ToAdaptySubscriptionPeriodUnit(this string value) - { - switch (value) - { - case "day": return AdaptySubscriptionPeriodUnit.Day; - case "week": return AdaptySubscriptionPeriodUnit.Week; - case "month": return AdaptySubscriptionPeriodUnit.Month; - case "year": return AdaptySubscriptionPeriodUnit.Year; - default: return AdaptySubscriptionPeriodUnit.Unknown; - } - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriodUnit+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriodUnit+JSON.cs.meta deleted file mode 100644 index a9c6284..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPeriodUnit+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c4cfe86a4a0534b77b2a494a90e4052a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPhase+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPhase+JSON.cs deleted file mode 100644 index 4577833..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPhase+JSON.cs +++ /dev/null @@ -1,57 +0,0 @@ -// -// AdaptySubscriptionPhase+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System; -using System.Collections.Generic; - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptySubscriptionPhase - { - internal AdaptySubscriptionPhase(JSONObject jsonNode) - { - Price = jsonNode.GetAdaptyPrice("price"); - NumberOfPeriods = jsonNode.GetInteger("number_of_periods"); - PaymentMode = jsonNode.GetAdaptyPaymentMode("payment_mode"); - SubscriptionPeriod = jsonNode.GetAdaptySubscriptionPeriod("subscription_period"); - LocalizedSubscriptionPeriod = jsonNode.GetStringIfPresent("localized_subscription_period"); - LocalizedNumberOfPeriods = jsonNode.GetStringIfPresent("localized_number_of_periods"); - } - } - -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptySubscriptionPhase GetAdaptySubscriptionPhase(this JSONNode node, string aKey) - => new AdaptySubscriptionPhase(GetObject(node, aKey)); - - internal static AdaptySubscriptionPhase GetAdaptySubscriptionPhaseIfPresent(this JSONNode node, string aKey) - { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) return null; - return new AdaptySubscriptionPhase(obj); - } - - internal static IList GetAdaptySubscriptionPhaseListIfPresent(this JSONNode node, string aKey) - { - var array = GetArrayIfPresent(node, aKey); - if (array is null) return null; - var result = new List(); - foreach (var item in array.Children) - { - if (!item.IsObject) throw new Exception($"Value by index: {result.Count} is not Object"); - result.Add(new AdaptySubscriptionPhase(item.AsObject)); - } - return result; - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPhase+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPhase+JSON.cs.meta deleted file mode 100644 index 31e028a..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionPhase+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: ac78235cd63304f87a77a72ce27e29f4 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionRenewalType+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionRenewalType+JSON.cs deleted file mode 100644 index cd63177..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionRenewalType+JSON.cs +++ /dev/null @@ -1,28 +0,0 @@ -// -// AdaptySubscriptionRenewalType+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptySubscriptionRenewalType GetAdaptySubscriptionRenewalType(this JSONNode node, string aKey) - => GetString(node, aKey).ToAdaptySubscriptionRenewalType(); - - internal static AdaptySubscriptionRenewalType? GetAdaptySubscriptionRenewalTypeIfPresent(this JSONNode node, string aKey) - => GetStringIfPresent(node, aKey)?.ToAdaptySubscriptionRenewalType(); - - private static AdaptySubscriptionRenewalType ToAdaptySubscriptionRenewalType(this string value) - { - switch (value) - { - case "prepaid": return AdaptySubscriptionRenewalType.Prepaid; - case "autorenewable": return AdaptySubscriptionRenewalType.Autorenewable; - default: return AdaptySubscriptionRenewalType.Autorenewable; - } - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionRenewalType+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionRenewalType+JSON.cs.meta deleted file mode 100644 index 668961a..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionRenewalType+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: dd5ad3689c2334c96af8e82b33beb845 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateParameters+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateParameters+JSON.cs deleted file mode 100644 index 884fb58..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateParameters+JSON.cs +++ /dev/null @@ -1,22 +0,0 @@ -// -// AdaptySubscriptionUpdateParameters+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptySubscriptionUpdateParameters - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - node.Add("old_sub_vendor_product_id", OldSubVendorProductId); - node.Add("replacement_mode", ReplacementMode.ToJSONNode()); - return node; - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateParameters+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateParameters+JSON.cs.meta deleted file mode 100644 index 56a0b9d..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateParameters+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a3b6957cb032f45a98bd634b0766aefc -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateReplacementMode+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateReplacementMode+JSON.cs deleted file mode 100644 index f4d6acc..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateReplacementMode+JSON.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// AdaptySubscriptionUpdateReplacementMode+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 25.11.2022. -// - -using System; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static JSONNode ToJSONNode(this AdaptySubscriptionUpdateReplacementMode value) - { - switch (value) - { - case AdaptySubscriptionUpdateReplacementMode.WithTimeProration: return "with_time_proration"; - case AdaptySubscriptionUpdateReplacementMode.ChargeProratedPrice: return "charge_prorated_price"; - case AdaptySubscriptionUpdateReplacementMode.WithoutProration: return "without_proration"; - case AdaptySubscriptionUpdateReplacementMode.Deferred: return "deferred"; - case AdaptySubscriptionUpdateReplacementMode.ChargeFullPrice: return "charge_full_price"; - default: throw new Exception($"AdaptySubscriptionUpdateReplacementMode unknown value: {value}"); - } - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateReplacementMode+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateReplacementMode+JSON.cs.meta deleted file mode 100644 index cd39123..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptySubscriptionUpdateReplacementMode+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9942f6b01661847658b627a95983368c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogActionType+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogActionType+JSON.cs deleted file mode 100644 index 43d5814..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogActionType+JSON.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// AdaptyUIDialogActionType+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 17.12.2024. -// -using System; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyUIDialogActionType GetAdaptyUIDialogActionType(this JSONNode node) => - GetString(node).ToAdaptyUIDialogActionType(); - internal static AdaptyUIDialogActionType GetAdaptyUIDialogActionType(this JSONNode node, string aKey) => - GetString(node, aKey).ToAdaptyUIDialogActionType(); - internal static AdaptyUIDialogActionType? GetAdaptyUIDialogActionTypeIfPresent(this JSONNode node, string aKey) => - GetStringIfPresent(node, aKey)?.ToAdaptyUIDialogActionType(); - private static AdaptyUIDialogActionType ToAdaptyUIDialogActionType(this string value) => - value switch - { - "primary" => AdaptyUIDialogActionType.Primary, - "secondary" => AdaptyUIDialogActionType.Secondary, - _ => throw new Exception($"AdaptyUIDialogActionType unknown value: {value}"), - }; - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogActionType+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogActionType+JSON.cs.meta deleted file mode 100644 index d1cff75..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogActionType+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: aab2120fca592469fa7a642697f4e43e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogConfiguration+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogConfiguration+JSON.cs deleted file mode 100644 index 7676533..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogConfiguration+JSON.cs +++ /dev/null @@ -1,25 +0,0 @@ -// -// AdaptyUIDialogConfiguration+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 07.09.2023. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyUIDialogConfiguration - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - if (Title != null) node.Add("title", Title); - if (Content != null) node.Add("content", Content); - node.Add("default_action_title", DefaultActionTitle); - if (SecondaryActionTitle != null) node.Add("secondary_action_title", SecondaryActionTitle); - return node; - } - } -} - diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogConfiguration+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogConfiguration+JSON.cs.meta deleted file mode 100644 index 853fa27..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIDialogConfiguration+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9927dc3d6a6064f108f968f3ab5628cf -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIFlowView+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIFlowView+JSON.cs deleted file mode 100644 index cb8784b..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIFlowView+JSON.cs +++ /dev/null @@ -1,32 +0,0 @@ -// -// AdaptyUIFlowView+JSON.cs -// AdaptySDK -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyUIFlowView - { - internal AdaptyUIFlowView(JSONObject jsonNode) - { - Id = jsonNode.GetString("id"); - PlacementId = jsonNode.GetString("placement_id"); - VariationId = jsonNode.GetString("variation_id"); - Locale = jsonNode.GetStringIfPresent("locale"); - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyUIFlowView GetAdaptyUIFlowView(this JSONNode node) => - new AdaptyUIFlowView(GetObject(node)); - - internal static AdaptyUIFlowView GetAdaptyUIFlowView(this JSONNode node, string aKey) => - new AdaptyUIFlowView(GetObject(node, aKey)); - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIFlowView+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIFlowView+JSON.cs.meta deleted file mode 100644 index df58457..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIFlowView+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3a1cd3a97c544405ba8165ae44a9dbac \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIIOSPresentationStyle+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIIOSPresentationStyle+JSON.cs deleted file mode 100644 index ab519ae..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIIOSPresentationStyle+JSON.cs +++ /dev/null @@ -1,22 +0,0 @@ -// -// AdaptyUIIOSPresentationStyle+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2024. -// - -using System; - -namespace AdaptySDK -{ - public static partial class AdaptyUIIOSPresentationStyleExtensions - { - public static string ToJSONNode(this AdaptyUIIOSPresentationStyle value) => - value switch - { - AdaptyUIIOSPresentationStyle.FullScreen => "full_screen", - AdaptyUIIOSPresentationStyle.PageSheet => "page_sheet", - _ => throw new Exception($"AdaptyUIIOSPresentationStyle unknown value: {value}"), - }; - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIIOSPresentationStyle+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIIOSPresentationStyle+JSON.cs.meta deleted file mode 100644 index 0b7408f..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIIOSPresentationStyle+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: ba803f958914d4c23b169cdee0544b3e \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIMediaCacheConfiguration+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIMediaCacheConfiguration+JSON.cs deleted file mode 100644 index cdfd16d..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIMediaCacheConfiguration+JSON.cs +++ /dev/null @@ -1,23 +0,0 @@ -// -// AdaptyUIMediaCacheConfiguration+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 07.09.2023. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyUIMediaCacheConfiguration - { - internal JSONNode ToJSONNode() - { - var node = new JSONObject(); - if (MemoryStorageTotalCostLimit.HasValue) node.Add("memory_storage_total_cost_limit", MemoryStorageTotalCostLimit.Value); - if (MemoryStorageCountLimit.HasValue) node.Add("memory_storage_count_limit", MemoryStorageCountLimit.Value); - if (DiskStorageSizeLimit.HasValue) node.Add("disk_storage_size_limit", DiskStorageSizeLimit.Value); - return node; - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIMediaCacheConfiguration+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIMediaCacheConfiguration+JSON.cs.meta deleted file mode 100644 index 5c7003a..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIMediaCacheConfiguration+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: f14276bc9754949838e9b1a2ce6af532 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingMeta+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingMeta+JSON.cs deleted file mode 100644 index d43f676..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingMeta+JSON.cs +++ /dev/null @@ -1,38 +0,0 @@ -// -// AdaptyUIOnboardingMeta+JSON.cs -// AdaptySDK -// -// Created by GPT-5 on 17.09.2025. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public sealed partial class AdaptyUIOnboardingMetaExtensions { } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyUIOnboardingMeta GetAdaptyUIOnboardingMeta( - this JSONNode node, - string aKey - ) - { - var obj = JSONNodeExtensions.GetObject(node, aKey); - // cross_platform.yaml uses keys: onboarding_id, screen_cid, screen_index, total_screens - var onboardingId = obj.GetString("onboarding_id"); - var screenCid = obj.GetString("screen_cid"); - var screenIndex = obj.GetInteger("screen_index"); - var totalScreens = obj.GetInteger("total_screens"); - return new AdaptySDK.AdaptyUIOnboardingMeta( - onboardingId, - screenCid, - screenIndex, - totalScreens - ); - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingMeta+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingMeta+JSON.cs.meta deleted file mode 100644 index fbb01fa..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingMeta+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: cfb2600791c54417aa722054f5c402da \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingView+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingView+JSON.cs deleted file mode 100644 index 7cb6077..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingView+JSON.cs +++ /dev/null @@ -1,35 +0,0 @@ -// -// AdaptyUIOnboardingView+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 17.12.2024. -// - -namespace AdaptySDK -{ - using AdaptySDK.SimpleJSON; - - public partial class AdaptyUIOnboardingView - { - internal AdaptyUIOnboardingView(JSONObject jsonNode) - { - Id = jsonNode.GetString("id"); - PlacementId = jsonNode.GetString("placement_id"); - PaywallVariationId = jsonNode.GetString("variation_id"); - } - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyUIOnboardingView GetAdaptyUIOnboardingView(this JSONNode node) => - new AdaptyUIOnboardingView(GetObject(node)); - - internal static AdaptyUIOnboardingView GetAdaptyUIOnboardingView( - this JSONNode node, - string aKey - ) => new AdaptyUIOnboardingView(GetObject(node, aKey)); - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingView+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingView+JSON.cs.meta deleted file mode 100644 index 39b3321..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIOnboardingView+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 824d299dd6bb24285a6b4f4b218238a3 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserAction+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserAction+JSON.cs deleted file mode 100644 index c62a2c3..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserAction+JSON.cs +++ /dev/null @@ -1,40 +0,0 @@ -// -// AdaptyUIUserAction+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 17.12.2024. -// - -using System; -using System.Collections.Generic; - -namespace AdaptySDK { - using AdaptySDK.SimpleJSON; - - public partial class AdaptyUIUserAction { - internal AdaptyUIUserAction(JSONObject jsonNode) { - Type = jsonNode.GetAdaptyUIUserActionType("type"); - Value = jsonNode.GetStringIfPresent("value"); - OpenIn = jsonNode.GetAdaptyWebPresentationIfPresent("open_in"); - } - } -} - -namespace AdaptySDK.SimpleJSON { - internal static partial class JSONNodeExtensions { - internal static AdaptyUIUserAction GetAdaptyUIUserAction(this JSONNode node, string aKey) => - new AdaptyUIUserAction(GetObject(node, aKey)); - - internal static AdaptyUIUserAction GetAdaptyUIUserActionIfPresent( - this JSONNode node, - string aKey - ) { - var obj = GetObjectIfPresent(node, aKey); - if (obj is null) { - return null; - } - - return new AdaptyUIUserAction(obj); - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserAction+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserAction+JSON.cs.meta deleted file mode 100644 index b3a18af..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserAction+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 68e2e82447e974f23aa1ce5cdc1408e8 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserActionType+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserActionType+JSON.cs deleted file mode 100644 index a24aa72..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserActionType+JSON.cs +++ /dev/null @@ -1,29 +0,0 @@ -// -// AdaptyUIUserActionType+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 17.12.2024. -// -using System; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyUIUserActionType GetAdaptyUIUserActionType(this JSONNode node, string aKey) => - GetString(node, aKey).ToAAdaptyUIUserActionType(); - - internal static AdaptyUIUserActionType? GetAdaptyUIUserActionTypeIfPresent(this JSONNode node, string aKey) => - GetStringIfPresent(node, aKey)?.ToAAdaptyUIUserActionType(); - - private static AdaptyUIUserActionType ToAAdaptyUIUserActionType(this string value) => - value switch - { - "close" => AdaptyUIUserActionType.Close, - "system_back" => AdaptyUIUserActionType.SystemBack, - "open_url" => AdaptyUIUserActionType.OpenUrl, - "custom" => AdaptyUIUserActionType.Custom, - _ => throw new Exception($"AdaptyUIUserActionType unknown value: {value}"), - }; - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserActionType+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserActionType+JSON.cs.meta deleted file mode 100644 index 9c881e2..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyUIUserActionType+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 380b1ca65765440a98e8eb8789fa7b1c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyWebPresentation+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyWebPresentation+JSON.cs deleted file mode 100644 index 998ee15..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyWebPresentation+JSON.cs +++ /dev/null @@ -1,43 +0,0 @@ -// -// AdaptyWebPresentation+JSON.cs -// AdaptySDK -// - -using System; - -namespace AdaptySDK -{ - public static partial class AdaptyWebPresentationExtensions - { - public static string ToJSONNode(this AdaptyWebPresentation value) => - value switch - { - AdaptyWebPresentation.ExternalBrowser => "browser_out_app", - AdaptyWebPresentation.InAppBrowser => "browser_in_app", - _ => throw new Exception($"AdaptyWebPresentation unknown value: {value}"), - }; - } -} - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static AdaptyWebPresentation GetAdaptyWebPresentation(this JSONNode node) => - GetString(node).ToAdaptyWebPresentation(); - - internal static AdaptyWebPresentation GetAdaptyWebPresentation(this JSONNode node, string aKey) => - GetString(node, aKey).ToAdaptyWebPresentation(); - - internal static AdaptyWebPresentation? GetAdaptyWebPresentationIfPresent(this JSONNode node, string aKey) => - GetStringIfPresent(node, aKey)?.ToAdaptyWebPresentation(); - - private static AdaptyWebPresentation ToAdaptyWebPresentation(this string value) => - value switch - { - "browser_out_app" => AdaptyWebPresentation.ExternalBrowser, - "browser_in_app" => AdaptyWebPresentation.InAppBrowser, - _ => throw new Exception($"AdaptyWebPresentation unknown value: {value}"), - }; - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyWebPresentation+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyWebPresentation+JSON.cs.meta deleted file mode 100644 index 910f633..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AdaptyWebPresentation+JSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: b2c3d4e5f6a74890b1c2d3e4f5a6b7c8 diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AppTrackingTransparencyStatus+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/AppTrackingTransparencyStatus+JSON.cs deleted file mode 100644 index c42efd6..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AppTrackingTransparencyStatus+JSON.cs +++ /dev/null @@ -1,26 +0,0 @@ -// -// AppTrackingTransparencyStatus+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static int ToJSON(this AppTrackingTransparencyStatus value) - { - switch (value) - { - case AppTrackingTransparencyStatus.NotDetermined: return 0; - case AppTrackingTransparencyStatus.Restricted: return 1; - case AppTrackingTransparencyStatus.Denied: return 2; - case AppTrackingTransparencyStatus.Authorized: return 3; - default: throw new Exception($"AppTrackingTransparencyStatus unknown value: {value}"); - } - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/AppTrackingTransparencyStatus+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/AppTrackingTransparencyStatus+JSON.cs.meta deleted file mode 100644 index 4abc437..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/AppTrackingTransparencyStatus+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 4889eb7147eff4890881bde68453e0d2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/DateTime+JSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/DateTime+JSON.cs deleted file mode 100644 index 2082f40..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/DateTime+JSON.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -// DateTime+JSON.cs -// Adapty -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System; -using System.Globalization; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - internal static JSONNode ToJSONNode(this DateTime value) - { - if (value.Kind != DateTimeKind.Utc) - { - value = value.ToUniversalTime(); - } - return value.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture); - } - - internal static DateTime GetDateTime(this JSONNode node, string aKey) => - node.GetString(aKey).ToDateTime(); - - internal static DateTime? GetDateTimeIfPresent(this JSONNode node, string aKey) - { - var str = node.GetStringIfPresent(aKey); - if (str is null) return null; - return str.ToDateTime(); - } - - private static DateTime ToDateTime(this string value) - { - try - { - return DateTime.Parse(value, CultureInfo.InvariantCulture); - } - catch (Exception e) - { - throw new Exception($"Exception on decoding DateTime from string: {e} source: \"{value}\""); - } - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/DateTime+JSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/DateTime+JSON.cs.meta deleted file mode 100644 index 3f10a97..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/DateTime+JSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d1350161cec4e433599b60795747ed1d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Collections.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Collections.cs deleted file mode 100644 index 944ac21..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Collections.cs +++ /dev/null @@ -1,203 +0,0 @@ -// -// SimpleJSON+Collections.cs -// Adapty -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System; -using System.Collections.Generic; - -namespace AdaptySDK.SimpleJSON -{ - public static partial class SimpleJSONCollections - { - public static IDictionary GetDictionary(this JSONNode node) => - node.AsObject.ToDictionary(); - - public static IDictionary GetDictionary(this JSONNode node, string aKey) => - node.GetObject(aKey).ToDictionary(); - - public static IDictionary GetDictionaryIfPresent( - this JSONNode node, - string aKey - ) => node.GetObjectIfPresent(aKey).ToDictionary(); - - public static IDictionary GetDictionaryIfPresent(this JSONNode node) => - node is null || node.IsNull ? null : node.AsObject.ToDictionary(); - - public static IList GetList(this JSONNode node) => node.AsArray.ToList(); - - public static IList GetList(this JSONNode node, string aKey) => - node.GetArray(aKey).ToList(); - - public static IList GetListIfPresent(this JSONNode node) => - node is null || node.IsNull ? null : node.AsArray.ToList(); - - public static IList GetListIfPresent(this JSONNode node, string aKey) => - node.GetArrayIfPresent(aKey).ToList(); - - private static IDictionary ToDictionary(this JSONObject obj) - { - if (obj == null) - return null; - - var result = new Dictionary(); - - foreach (var item in obj) - { - switch (item.Value.Tag) - { - case JSONNodeType.Array: - result.Add(item.Key, ToList(item.Value.AsArray)); - break; - case JSONNodeType.Object: - result.Add(item.Key, ToDictionary(item.Value.AsObject)); - break; - case JSONNodeType.Boolean: - result.Add(item.Key, item.Value.AsBool); - break; - case JSONNodeType.String: - result.Add(item.Key, item.Value.Value); - break; - case JSONNodeType.Number: - result.Add(item.Key, item.Value.AsDouble); - break; - case JSONNodeType.NullValue: - result.Add(item.Key, null); - break; - } - } - return result; - } - - private static IList ToList(this JSONArray obj) - { - if (obj == null) - return null; - - var result = new List(); - - foreach (var item in obj.Children) - { - switch (item.Tag) - { - case JSONNodeType.Array: - result.Add(ToList(item.AsArray)); - break; - case JSONNodeType.Object: - result.Add(ToDictionary(item.AsObject)); - break; - case JSONNodeType.Boolean: - result.Add(item.AsBool); - break; - case JSONNodeType.String: - result.Add(item.Value); - break; - case JSONNodeType.Number: - result.Add(item.AsDouble); - break; - case JSONNodeType.NullValue: - result.Add(null); - break; - } - } - return result; - } - - public static JSONObject ToJSONObject(this IDictionary obj) - { - var result = new JSONObject(); - - foreach (var item in obj) - { - if (item.Value is JSONNode) - { - result.Add(item.Key, item.Value as JSONNode); - } - else if (item.Value is Dictionary) - { - result.Add(item.Key, ToJSONObject(item.Value as Dictionary)); - } - else if (item.Value is IList) - { - result.Add(item.Key, ToJSONArray(item.Value as IList)); - } - else if (item.Value is null) - { - result.Add(item.Key, JSONNull.CreateOrGet()); - } - else if (item.Value is string) - { - result.Add(item.Key, new JSONString(item.Value as string)); - } - else if ( - item.Value is int - || item.Value is uint - || item.Value is long - || item.Value is ulong - || item.Value is short - || item.Value is ushort - || item.Value is sbyte - || item.Value is byte - ) - { - result.Add(item.Key, new JSONNumber(Convert.ToInt64((object)item.Value))); - } - else if (item.Value is float || item.Value is double || item.Value is decimal) - { - result.Add(item.Key, new JSONNumber(Convert.ToDouble((object)item.Value))); - } - } - - return result; - } - - public static JSONArray ToJSONArray(this IList obj) - { - var result = new JSONArray(); - - foreach (var item in obj) - { - if (item is JSONNode) - { - result.Add(item as JSONNode); - } - else if (item is Dictionary) - { - result.Add(ToJSONObject(item as Dictionary)); - } - else if (item is IList) - { - result.Add(ToJSONArray(item as IList)); - } - else if (item is null) - { - result.Add(JSONNull.CreateOrGet()); - } - else if (item is string) - { - result.Add(new JSONString(item as string)); - } - else if ( - item is int - || item is uint - || item is long - || item is ulong - || item is short - || item is ushort - || item is sbyte - || item is byte - ) - { - result.Add(new JSONNumber(Convert.ToInt64((object)item))); - } - else if (item is float || item is double || item is decimal) - { - result.Add(new JSONNumber(Convert.ToDouble((object)item))); - } - } - return result; - } - } -} diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Collections.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Collections.cs.meta deleted file mode 100644 index cb3d80a..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Collections.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 8c0c9d386dd664d54af5e49ddd8cea3f -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Extensions.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Extensions.cs deleted file mode 100644 index bf88b1a..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Extensions.cs +++ /dev/null @@ -1,264 +0,0 @@ -// -// SimpleJSON+Extensions.cs -// Adapty -// -// Created by Aleksei Valiano on 20.12.2022. -// - -using System.Collections.Generic; -using System; - -namespace AdaptySDK.SimpleJSON -{ - internal static partial class JSONNodeExtensions - { - private static JSONNode GetJSONNodeIfPresent(this JSONNode node) - { - if (node is null) return null; - if (node.IsNull) return null; - return node; - } - - private static JSONNode GetJSONNodeIfPresent(this JSONNode node, string aKey) - { - if (node is null) throw new Exception("JSON node is Null"); - if (!node.IsObject) throw new Exception("JSON node is not Object"); - if (!node.HasKey(aKey)) return null; - var valueNode = node[aKey]; - if (valueNode.IsNull) return null; - return valueNode; - } - - private static JSONNode GetJSONNode(this JSONNode node) - { - if (node is null) throw new Exception("JSON node is Null"); - return node; - } - - private static JSONNode GetJSONNode(this JSONNode node, string aKey) - { - if (node is null) throw new Exception("JSON node is Null"); - if (!node.IsObject) throw new Exception("JSON node is not Object"); - if (!node.HasKey(aKey)) throw new Exception($"Object has not key: {aKey}"); - return node[aKey]; - } - - internal static string GetString(this JSONNode node) - { - JSONNode valueNode = GetJSONNode(node); - if (!valueNode.IsString) throw new Exception($"Value is not String"); - return valueNode.Value; - } - - internal static string GetString(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNode(node, aKey); - if (!valueNode.IsString) throw new Exception($"Value by key: {aKey} is not String"); - return valueNode.Value; - } - - internal static string GetStringIfPresent(this JSONNode node) - { - JSONNode valueNode = GetJSONNodeIfPresent(node); - if (valueNode is null) return null; - if (!valueNode.IsString) throw new Exception($"Value is not String"); - return valueNode.Value; - } - - internal static string GetStringIfPresent(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNodeIfPresent(node, aKey); - if (valueNode is null) return null; - if (!valueNode.IsString) throw new Exception($"Value by key: {aKey} is not String"); - return valueNode.Value; - } - - internal static IList GetStringListIfPresent(this JSONNode node) - { - var array = GetArrayIfPresent(node); - if (array is null) return null; - var result = new List(); - foreach (var item in array.Children) - { - if (!item.IsString) throw new Exception($"Value by index: {result.Count} is not String"); - result.Add(item.Value); - } - return result; - } - - internal static IList GetStringListIfPresent(this JSONNode node, string aKey) - { - var array = GetArrayIfPresent(node, aKey); - if (array is null) return null; - var result = new List(); - foreach (var item in array.Children) - { - if (!item.IsString) throw new Exception($"Value by index: {result.Count} is not String"); - result.Add(item.Value); - } - return result; - } - - internal static double GetDouble(this JSONNode node) - { - JSONNode valueNode = GetJSONNode(node); - if (!valueNode.IsNumber) throw new Exception($"Value is not Number"); - return valueNode.AsDouble; - } - - internal static double GetDouble(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNode(node, aKey); - if (!valueNode.IsNumber) throw new Exception($"Value by key: {aKey} is not Number"); - return valueNode.AsDouble; - } - - internal static double? GetDoubleIfPresent(this JSONNode node) - { - JSONNode valueNode = GetJSONNodeIfPresent(node); - if (valueNode is null) return null; - if (!valueNode.IsNumber) throw new Exception($"Value is not Number"); - return valueNode.AsDouble; - } - - internal static double? GetDoubleIfPresent(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNodeIfPresent(node, aKey); - if (valueNode is null) return null; - if (!valueNode.IsNumber) throw new Exception($"Value by key: {aKey} is not Number"); - return valueNode.AsDouble; - } - - internal static int GetInteger(this JSONNode node) => (int)node.GetDouble(); - - internal static int GetInteger(this JSONNode node, string aKey) => (int)node.GetDouble(aKey); - - internal static int? GetIntegerIfPresent(this JSONNode node) => (int)node.GetDoubleIfPresent(); - - internal static int? GetIntegerIfPresent(this JSONNode node, string aKey) => (int)node.GetDoubleIfPresent(aKey); - - internal static float GetFloat(this JSONNode node, string aKey) => (float)node.GetDouble(aKey); - - internal static float? GetFloatIfPresent(this JSONNode node, string aKey) => (float)node.GetDoubleIfPresent(aKey); - - internal static long GetLong(this JSONNode node) - { - JSONNode valueNode = GetJSONNode(node); - if (!valueNode.IsNumber) throw new Exception($"Value is not Number"); - return valueNode.AsLong; - } - - internal static long GetLong(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNode(node, aKey); - if (!valueNode.IsNumber) throw new Exception($"Value by key: {aKey} is not Number"); - return valueNode.AsLong; - } - - internal static long? GetLongIfPresent(this JSONNode node) - { - JSONNode valueNode = GetJSONNodeIfPresent(node); - if (valueNode is null) return null; - if (!valueNode.IsNumber) throw new Exception($"Value is not Number"); - return valueNode.AsLong; - } - - internal static long? GetLongIfPresent(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNodeIfPresent(node, aKey); - if (valueNode is null) return null; - if (!valueNode.IsNumber) throw new Exception($"Value by key: {aKey} is not Number"); - return valueNode.AsLong; - } - - internal static bool GetBoolean(this JSONNode node) - { - JSONNode valueNode = GetJSONNode(node); - if (!valueNode.IsBoolean) throw new Exception($"Value is not Bool"); - return valueNode.AsBool; - } - - internal static bool GetBoolean(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNode(node, aKey); - if (!valueNode.IsBoolean) throw new Exception($"Value by key: {aKey} is not Bool"); - return valueNode.AsBool; - } - - internal static bool? GetBooleanIfPresent(this JSONNode node) - { - JSONNode valueNode = GetJSONNodeIfPresent(node); - if (valueNode is null) return null; - if (!valueNode.IsBoolean) throw new Exception($"Value is not Bool"); - return valueNode.AsBool; - } - - internal static bool? GetBooleanIfPresent(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNodeIfPresent(node, aKey); - if (valueNode is null) return null; - if (!valueNode.IsBoolean) throw new Exception($"Value by key: {aKey} is not Bool"); - return valueNode.AsBool; - } - - internal static JSONArray GetArray(this JSONNode node) - { - JSONNode valueNode = GetJSONNode(node); - if (!valueNode.IsArray) throw new Exception($"Value is not Array"); - return valueNode.AsArray; - } - - internal static JSONArray GetArray(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNode(node, aKey); - if (!valueNode.IsArray) throw new Exception($"Value by key: {aKey} is not Array"); - return valueNode.AsArray; - } - - internal static JSONArray GetArrayIfPresent(this JSONNode node) - { - JSONNode valueNode = GetJSONNodeIfPresent(node); - if (valueNode is null) return null; - if (!valueNode.IsArray) throw new Exception($"Value is not Array"); - return valueNode.AsArray; - } - - internal static JSONArray GetArrayIfPresent(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNodeIfPresent(node, aKey); - if (valueNode is null) return null; - if (!valueNode.IsArray) throw new Exception($"Value by key: {aKey} is not Array"); - return valueNode.AsArray; - } - - internal static JSONObject GetObject(this JSONNode node) - { - JSONNode valueNode = GetJSONNode(node); - if (!valueNode.IsObject) throw new Exception($"Value is not Object"); - return valueNode.AsObject; - } - - internal static JSONObject GetObject(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNode(node, aKey); - if (!valueNode.IsObject) throw new Exception($"Value by key: {aKey} is not Object"); - return valueNode.AsObject; - } - - internal static JSONObject GetObjectIfPresent(this JSONNode node) - { - JSONNode valueNode = GetJSONNodeIfPresent(node); - if (valueNode is null) return null; - if (!valueNode.IsObject) throw new Exception($"Value is not Object"); - return valueNode.AsObject; - } - - internal static JSONObject GetObjectIfPresent(this JSONNode node, string aKey) - { - JSONNode valueNode = GetJSONNodeIfPresent(node, aKey); - if (valueNode is null) return null; - if (!valueNode.IsObject) throw new Exception($"Value by key: {aKey} is not Object"); - return valueNode.AsObject; - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Extensions.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Extensions.cs.meta deleted file mode 100644 index 3f75f1b..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON+Extensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 3faa258fe462f40b184a5c69db314a50 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON.cs b/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON.cs deleted file mode 100644 index 6bbb3e8..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON.cs +++ /dev/null @@ -1,1349 +0,0 @@ -/* * * * * - * A simple JSON Parser / builder - * ------------------------------ - * - * It mainly has been written as a simple JSON parser. It can build a JSON string - * from the node-tree, or generate a node tree from any valid JSON string. - * - * Written by Bunny83 - * 2012-06-09 - * - * Changelog now external. See Changelog.txt - * - * The MIT License (MIT) - * - * Copyright (c) 2012-2019 Markus Göbel (Bunny83) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - * * * * */ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; - -namespace AdaptySDK.SimpleJSON -{ - public enum JSONNodeType - { - Array = 1, - Object = 2, - String = 3, - Number = 4, - NullValue = 5, - Boolean = 6, - None = 7, - Custom = 0xFF, - } - public enum JSONTextMode - { - Compact, - Indent - } - - public abstract partial class JSONNode - { - #region Enumerators - public struct Enumerator - { - private enum Type { None, Array, Object } - private Type type; - private Dictionary.Enumerator m_Object; - private List.Enumerator m_Array; - public bool IsValid { get { return type != Type.None; } } - public Enumerator(List.Enumerator aArrayEnum) - { - type = Type.Array; - m_Object = default(Dictionary.Enumerator); - m_Array = aArrayEnum; - } - public Enumerator(Dictionary.Enumerator aDictEnum) - { - type = Type.Object; - m_Object = aDictEnum; - m_Array = default(List.Enumerator); - } - public KeyValuePair Current - { - get - { - if (type == Type.Array) - return new KeyValuePair(string.Empty, m_Array.Current); - else if (type == Type.Object) - return m_Object.Current; - return new KeyValuePair(string.Empty, null); - } - } - public bool MoveNext() - { - if (type == Type.Array) - return m_Array.MoveNext(); - else if (type == Type.Object) - return m_Object.MoveNext(); - return false; - } - } - public struct ValueEnumerator - { - private Enumerator m_Enumerator; - public ValueEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } - public ValueEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } - public ValueEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } - public JSONNode Current { get { return m_Enumerator.Current.Value; } } - public bool MoveNext() { return m_Enumerator.MoveNext(); } - public ValueEnumerator GetEnumerator() { return this; } - } - public struct KeyEnumerator - { - private Enumerator m_Enumerator; - public KeyEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } - public KeyEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } - public KeyEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } - public string Current { get { return m_Enumerator.Current.Key; } } - public bool MoveNext() { return m_Enumerator.MoveNext(); } - public KeyEnumerator GetEnumerator() { return this; } - } - public class LinqEnumerator : IEnumerator>, IEnumerable> - { - private JSONNode m_Node; - private Enumerator m_Enumerator; - internal LinqEnumerator(JSONNode aNode) - { - m_Node = aNode; - if (m_Node != null) - m_Enumerator = m_Node.GetEnumerator(); - } - public KeyValuePair Current { get { return m_Enumerator.Current; } } - object IEnumerator.Current { get { return m_Enumerator.Current; } } - public bool MoveNext() { return m_Enumerator.MoveNext(); } - - public void Dispose() - { - m_Node = null; - m_Enumerator = new Enumerator(); - } - - public IEnumerator> GetEnumerator() - { - return new LinqEnumerator(m_Node); - } - - public void Reset() - { - if (m_Node != null) - m_Enumerator = m_Node.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new LinqEnumerator(m_Node); - } - } - #endregion Enumerators - - #region common interface - public static bool forceASCII = false; // Use Unicode by default - public static bool longAsString = false; // lazy creator creates a JSONString instead of JSONNumber - public static bool allowLineComments = true; // allow "//"-style comments at the end of a line - - public abstract JSONNodeType Tag { get; } - - public virtual JSONNode this[int aIndex] { get { return null; } set { } } - - public virtual JSONNode this[string aKey] { get { return null; } set { } } - - public virtual string Value { get { return ""; } set { } } - - public virtual int Count { get { return 0; } } - - public virtual bool IsNumber { get { return false; } } - public virtual bool IsString { get { return false; } } - public virtual bool IsBoolean { get { return false; } } - public virtual bool IsNull { get { return false; } } - public virtual bool IsArray { get { return false; } } - public virtual bool IsObject { get { return false; } } - - public virtual bool Inline { get { return false; } set { } } - - public virtual void Add(string aKey, JSONNode aItem) - { - } - public virtual void Add(JSONNode aItem) - { - Add("", aItem); - } - - public virtual JSONNode Remove(string aKey) - { - return null; - } - - public virtual JSONNode Remove(int aIndex) - { - return null; - } - - public virtual JSONNode Remove(JSONNode aNode) - { - return aNode; - } - - public virtual JSONNode Clone() - { - return null; - } - - public virtual IEnumerable Children - { - get - { - yield break; - } - } - - public IEnumerable DeepChildren - { - get - { - foreach (var C in Children) - foreach (var D in C.DeepChildren) - yield return D; - } - } - - public virtual bool HasKey(string aKey) - { - return false; - } - - public virtual JSONNode GetValueOrDefault(string aKey, JSONNode aDefault) - { - return aDefault; - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - WriteToStringBuilder(sb, 0, 0, JSONTextMode.Compact); - return sb.ToString(); - } - - public virtual string ToString(int aIndent) - { - StringBuilder sb = new StringBuilder(); - WriteToStringBuilder(sb, 0, aIndent, JSONTextMode.Indent); - return sb.ToString(); - } - internal abstract void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode); - - public abstract Enumerator GetEnumerator(); - public IEnumerable> Linq { get { return new LinqEnumerator(this); } } - public KeyEnumerator Keys { get { return new KeyEnumerator(GetEnumerator()); } } - public ValueEnumerator Values { get { return new ValueEnumerator(GetEnumerator()); } } - - #endregion common interface - - #region typecasting properties - - - public virtual double AsDouble - { - get - { - double v = 0.0; - if (double.TryParse(Value, NumberStyles.Float, CultureInfo.InvariantCulture, out v)) - return v; - return 0.0; - } - set - { - Value = value.ToString(CultureInfo.InvariantCulture); - } - } - - public virtual int AsInt - { - get { return (int)AsDouble; } - set { AsDouble = value; } - } - - public virtual float AsFloat - { - get { return (float)AsDouble; } - set { AsDouble = value; } - } - - public virtual bool AsBool - { - get - { - bool v = false; - if (bool.TryParse(Value, out v)) - return v; - return !string.IsNullOrEmpty(Value); - } - set - { - Value = (value) ? "true" : "false"; - } - } - - public virtual long AsLong - { - get - { - long val = 0; - if (long.TryParse(Value, out val)) - return val; - return 0L; - } - set - { - Value = value.ToString(); - } - } - - public virtual JSONArray AsArray - { - get - { - return this as JSONArray; - } - } - - public virtual JSONObject AsObject - { - get - { - return this as JSONObject; - } - } - - - #endregion typecasting properties - - #region operators - - public static implicit operator JSONNode(string s) - { - return new JSONString(s); - } - public static implicit operator string(JSONNode d) - { - return (d == null) ? null : d.Value; - } - - public static implicit operator JSONNode(double n) - { - return new JSONNumber(n); - } - public static implicit operator double(JSONNode d) - { - return (d == null) ? 0 : d.AsDouble; - } - - public static implicit operator JSONNode(float n) - { - return new JSONNumber(n); - } - public static implicit operator float(JSONNode d) - { - return (d == null) ? 0 : d.AsFloat; - } - - public static implicit operator JSONNode(int n) - { - return new JSONNumber(n); - } - public static implicit operator int(JSONNode d) - { - return (d == null) ? 0 : d.AsInt; - } - - public static implicit operator JSONNode(long n) - { - if (longAsString) - return new JSONString(n.ToString()); - return new JSONNumber(n); - } - public static implicit operator long(JSONNode d) - { - return (d == null) ? 0L : d.AsLong; - } - - public static implicit operator JSONNode(bool b) - { - return new JSONBool(b); - } - public static implicit operator bool(JSONNode d) - { - return (d == null) ? false : d.AsBool; - } - - public static implicit operator JSONNode(KeyValuePair aKeyValue) - { - return aKeyValue.Value; - } - - public static bool operator ==(JSONNode a, object b) - { - if (ReferenceEquals(a, b)) - return true; - bool aIsNull = a is JSONNull || ReferenceEquals(a, null) || a is JSONLazyCreator; - bool bIsNull = b is JSONNull || ReferenceEquals(b, null) || b is JSONLazyCreator; - if (aIsNull && bIsNull) - return true; - return !aIsNull && a.Equals(b); - } - - public static bool operator !=(JSONNode a, object b) - { - return !(a == b); - } - - public override bool Equals(object obj) - { - return ReferenceEquals(this, obj); - } - - public override int GetHashCode() - { - return base.GetHashCode(); - } - - #endregion operators - - [ThreadStatic] - private static StringBuilder m_EscapeBuilder; - internal static StringBuilder EscapeBuilder - { - get - { - if (m_EscapeBuilder == null) - m_EscapeBuilder = new StringBuilder(); - return m_EscapeBuilder; - } - } - internal static string Escape(string aText) - { - var sb = EscapeBuilder; - sb.Length = 0; - if (sb.Capacity < aText.Length + aText.Length / 10) - sb.Capacity = aText.Length + aText.Length / 10; - foreach (char c in aText) - { - switch (c) - { - case '\\': - sb.Append("\\\\"); - break; - case '\"': - sb.Append("\\\""); - break; - case '\n': - sb.Append("\\n"); - break; - case '\r': - sb.Append("\\r"); - break; - case '\t': - sb.Append("\\t"); - break; - case '\b': - sb.Append("\\b"); - break; - case '\f': - sb.Append("\\f"); - break; - default: - if (c < ' ' || (forceASCII && c > 127)) - { - ushort val = c; - sb.Append("\\u").Append(val.ToString("X4")); - } - else - sb.Append(c); - break; - } - } - string result = sb.ToString(); - sb.Length = 0; - return result; - } - - private static JSONNode ParseElement(string token, bool quoted) - { - if (quoted) - return token; - string tmp = token.ToLower(); - if (tmp == "false" || tmp == "true") - return tmp == "true"; - if (tmp == "null") - return JSONNull.CreateOrGet(); - double val; - if (double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out val)) - return val; - else - return token; - } - - public static JSONNode Parse(string aJSON) - { - Stack stack = new Stack(); - JSONNode ctx = null; - int i = 0; - StringBuilder Token = new StringBuilder(); - string TokenName = ""; - bool QuoteMode = false; - bool TokenIsQuoted = false; - while (i < aJSON.Length) - { - switch (aJSON[i]) - { - case '{': - if (QuoteMode) - { - Token.Append(aJSON[i]); - break; - } - stack.Push(new JSONObject()); - if (ctx != null) - { - ctx.Add(TokenName, stack.Peek()); - } - TokenName = ""; - Token.Length = 0; - ctx = stack.Peek(); - break; - - case '[': - if (QuoteMode) - { - Token.Append(aJSON[i]); - break; - } - - stack.Push(new JSONArray()); - if (ctx != null) - { - ctx.Add(TokenName, stack.Peek()); - } - TokenName = ""; - Token.Length = 0; - ctx = stack.Peek(); - break; - - case '}': - case ']': - if (QuoteMode) - { - - Token.Append(aJSON[i]); - break; - } - if (stack.Count == 0) - throw new Exception("JSON Parse: Too many closing brackets"); - - stack.Pop(); - if (Token.Length > 0 || TokenIsQuoted) - ctx.Add(TokenName, ParseElement(Token.ToString(), TokenIsQuoted)); - TokenIsQuoted = false; - TokenName = ""; - Token.Length = 0; - if (stack.Count > 0) - ctx = stack.Peek(); - break; - - case ':': - if (QuoteMode) - { - Token.Append(aJSON[i]); - break; - } - TokenName = Token.ToString(); - Token.Length = 0; - TokenIsQuoted = false; - break; - - case '"': - QuoteMode ^= true; - TokenIsQuoted |= QuoteMode; - break; - - case ',': - if (QuoteMode) - { - Token.Append(aJSON[i]); - break; - } - if (Token.Length > 0 || TokenIsQuoted) - ctx.Add(TokenName, ParseElement(Token.ToString(), TokenIsQuoted)); - TokenIsQuoted = false; - TokenName = ""; - Token.Length = 0; - TokenIsQuoted = false; - break; - - case '\r': - case '\n': - break; - - case ' ': - case '\t': - if (QuoteMode) - Token.Append(aJSON[i]); - break; - - case '\\': - ++i; - if (QuoteMode) - { - char C = aJSON[i]; - switch (C) - { - case 't': - Token.Append('\t'); - break; - case 'r': - Token.Append('\r'); - break; - case 'n': - Token.Append('\n'); - break; - case 'b': - Token.Append('\b'); - break; - case 'f': - Token.Append('\f'); - break; - case 'u': - { - string s = aJSON.Substring(i + 1, 4); - Token.Append((char)int.Parse( - s, - System.Globalization.NumberStyles.AllowHexSpecifier)); - i += 4; - break; - } - default: - Token.Append(C); - break; - } - } - break; - case '/': - if (allowLineComments && !QuoteMode && i + 1 < aJSON.Length && aJSON[i + 1] == '/') - { - while (++i < aJSON.Length && aJSON[i] != '\n' && aJSON[i] != '\r') ; - break; - } - Token.Append(aJSON[i]); - break; - case '\uFEFF': // remove / ignore BOM (Byte Order Mark) - break; - - default: - Token.Append(aJSON[i]); - break; - } - ++i; - } - if (QuoteMode) - { - throw new Exception("JSON Parse: Quotation marks seems to be messed up."); - } - if (ctx == null) - return ParseElement(Token.ToString(), TokenIsQuoted); - return ctx; - } - - } - // End of JSONNode - - public partial class JSONArray : JSONNode - { - private List m_List = new List(); - private bool inline = false; - public override bool Inline - { - get { return inline; } - set { inline = value; } - } - - public override JSONNodeType Tag { get { return JSONNodeType.Array; } } - public override bool IsArray { get { return true; } } - public override Enumerator GetEnumerator() { return new Enumerator(m_List.GetEnumerator()); } - - public override JSONNode this[int aIndex] - { - get - { - if (aIndex < 0 || aIndex >= m_List.Count) - return new JSONLazyCreator(this); - return m_List[aIndex]; - } - set - { - if (value == null) - value = JSONNull.CreateOrGet(); - if (aIndex < 0 || aIndex >= m_List.Count) - m_List.Add(value); - else - m_List[aIndex] = value; - } - } - - public override JSONNode this[string aKey] - { - get { return new JSONLazyCreator(this); } - set - { - if (value == null) - value = JSONNull.CreateOrGet(); - m_List.Add(value); - } - } - - public override int Count - { - get { return m_List.Count; } - } - - public override void Add(string aKey, JSONNode aItem) - { - if (aItem == null) - aItem = JSONNull.CreateOrGet(); - m_List.Add(aItem); - } - - public override JSONNode Remove(int aIndex) - { - if (aIndex < 0 || aIndex >= m_List.Count) - return null; - JSONNode tmp = m_List[aIndex]; - m_List.RemoveAt(aIndex); - return tmp; - } - - public override JSONNode Remove(JSONNode aNode) - { - m_List.Remove(aNode); - return aNode; - } - - public override JSONNode Clone() - { - var node = new JSONArray(); - node.m_List.Capacity = m_List.Capacity; - foreach (var n in m_List) - { - if (n != null) - node.Add(n.Clone()); - else - node.Add(null); - } - return node; - } - - public override IEnumerable Children - { - get - { - foreach (JSONNode N in m_List) - yield return N; - } - } - - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append('['); - int count = m_List.Count; - if (inline) - aMode = JSONTextMode.Compact; - for (int i = 0; i < count; i++) - { - if (i > 0) - aSB.Append(','); - if (aMode == JSONTextMode.Indent) - aSB.AppendLine(); - - if (aMode == JSONTextMode.Indent) - aSB.Append(' ', aIndent + aIndentInc); - m_List[i].WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); - } - if (aMode == JSONTextMode.Indent) - aSB.AppendLine().Append(' ', aIndent); - aSB.Append(']'); - } - } - // End of JSONArray - - public partial class JSONObject : JSONNode - { - private Dictionary m_Dict = new Dictionary(); - - private bool inline = false; - public override bool Inline - { - get { return inline; } - set { inline = value; } - } - - public override JSONNodeType Tag { get { return JSONNodeType.Object; } } - public override bool IsObject { get { return true; } } - - public override Enumerator GetEnumerator() { return new Enumerator(m_Dict.GetEnumerator()); } - - - public override JSONNode this[string aKey] - { - get - { - if (m_Dict.ContainsKey(aKey)) - return m_Dict[aKey]; - else - return new JSONLazyCreator(this, aKey); - } - set - { - if (value == null) - value = JSONNull.CreateOrGet(); - if (m_Dict.ContainsKey(aKey)) - m_Dict[aKey] = value; - else - m_Dict.Add(aKey, value); - } - } - - public override JSONNode this[int aIndex] - { - get - { - if (aIndex < 0 || aIndex >= m_Dict.Count) - return null; - return m_Dict.ElementAt(aIndex).Value; - } - set - { - if (value == null) - value = JSONNull.CreateOrGet(); - if (aIndex < 0 || aIndex >= m_Dict.Count) - return; - string key = m_Dict.ElementAt(aIndex).Key; - m_Dict[key] = value; - } - } - - public override int Count - { - get { return m_Dict.Count; } - } - - public override void Add(string aKey, JSONNode aItem) - { - if (aItem == null) - aItem = JSONNull.CreateOrGet(); - - if (aKey != null) - { - if (m_Dict.ContainsKey(aKey)) - m_Dict[aKey] = aItem; - else - m_Dict.Add(aKey, aItem); - } - else - m_Dict.Add(Guid.NewGuid().ToString(), aItem); - } - - public override JSONNode Remove(string aKey) - { - if (!m_Dict.ContainsKey(aKey)) - return null; - JSONNode tmp = m_Dict[aKey]; - m_Dict.Remove(aKey); - return tmp; - } - - public override JSONNode Remove(int aIndex) - { - if (aIndex < 0 || aIndex >= m_Dict.Count) - return null; - var item = m_Dict.ElementAt(aIndex); - m_Dict.Remove(item.Key); - return item.Value; - } - - public override JSONNode Remove(JSONNode aNode) - { - try - { - var item = m_Dict.Where(k => k.Value == aNode).First(); - m_Dict.Remove(item.Key); - return aNode; - } - catch - { - return null; - } - } - - public override JSONNode Clone() - { - var node = new JSONObject(); - foreach (var n in m_Dict) - { - node.Add(n.Key, n.Value.Clone()); - } - return node; - } - - public override bool HasKey(string aKey) - { - return m_Dict.ContainsKey(aKey); - } - - public override JSONNode GetValueOrDefault(string aKey, JSONNode aDefault) - { - JSONNode res; - if (m_Dict.TryGetValue(aKey, out res)) - return res; - return aDefault; - } - - public override IEnumerable Children - { - get - { - foreach (KeyValuePair N in m_Dict) - yield return N.Value; - } - } - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append('{'); - bool first = true; - if (inline) - aMode = JSONTextMode.Compact; - foreach (var k in m_Dict) - { - if (!first) - aSB.Append(','); - first = false; - if (aMode == JSONTextMode.Indent) - aSB.AppendLine(); - if (aMode == JSONTextMode.Indent) - aSB.Append(' ', aIndent + aIndentInc); - aSB.Append('\"').Append(Escape(k.Key)).Append('\"'); - if (aMode == JSONTextMode.Compact) - aSB.Append(':'); - else - aSB.Append(" : "); - k.Value.WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); - } - if (aMode == JSONTextMode.Indent) - aSB.AppendLine().Append(' ', aIndent); - aSB.Append('}'); - } - - } - // End of JSONObject - - public partial class JSONString : JSONNode - { - private string m_Data; - - public override JSONNodeType Tag { get { return JSONNodeType.String; } } - public override bool IsString { get { return true; } } - - public override Enumerator GetEnumerator() { return new Enumerator(); } - - - public override string Value - { - get { return m_Data; } - set - { - m_Data = value; - } - } - - public JSONString(string aData) - { - m_Data = aData; - } - public override JSONNode Clone() - { - return new JSONString(m_Data); - } - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append('\"').Append(Escape(m_Data)).Append('\"'); - } - public override bool Equals(object obj) - { - if (base.Equals(obj)) - return true; - string s = obj as string; - if (s != null) - return m_Data == s; - JSONString s2 = obj as JSONString; - if (s2 != null) - return m_Data == s2.m_Data; - return false; - } - public override int GetHashCode() - { - return m_Data.GetHashCode(); - } - } - // End of JSONString - - public partial class JSONNumber : JSONNode - { - private double m_Data; - - public override JSONNodeType Tag { get { return JSONNodeType.Number; } } - public override bool IsNumber { get { return true; } } - public override Enumerator GetEnumerator() { return new Enumerator(); } - - public override string Value - { - get { return m_Data.ToString(CultureInfo.InvariantCulture); } - set - { - double v; - if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out v)) - m_Data = v; - } - } - - public override double AsDouble - { - get { return m_Data; } - set { m_Data = value; } - } - public override long AsLong - { - get { return (long)m_Data; } - set { m_Data = value; } - } - - public JSONNumber(double aData) - { - m_Data = aData; - } - - public JSONNumber(string aData) - { - Value = aData; - } - - public override JSONNode Clone() - { - return new JSONNumber(m_Data); - } - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append(Value); - } - private static bool IsNumeric(object value) - { - return value is int || value is uint - || value is float || value is double - || value is decimal - || value is long || value is ulong - || value is short || value is ushort - || value is sbyte || value is byte; - } - public override bool Equals(object obj) - { - if (obj == null) - return false; - if (base.Equals(obj)) - return true; - JSONNumber s2 = obj as JSONNumber; - if (s2 != null) - return m_Data == s2.m_Data; - if (IsNumeric(obj)) - return Convert.ToDouble(obj) == m_Data; - return false; - } - public override int GetHashCode() - { - return m_Data.GetHashCode(); - } - } - // End of JSONNumber - - public partial class JSONBool : JSONNode - { - private bool m_Data; - - public override JSONNodeType Tag { get { return JSONNodeType.Boolean; } } - public override bool IsBoolean { get { return true; } } - public override Enumerator GetEnumerator() { return new Enumerator(); } - - public override string Value - { - get { return m_Data.ToString(); } - set - { - bool v; - if (bool.TryParse(value, out v)) - m_Data = v; - } - } - public override bool AsBool - { - get { return m_Data; } - set { m_Data = value; } - } - - public JSONBool(bool aData) - { - m_Data = aData; - } - - public JSONBool(string aData) - { - Value = aData; - } - - public override JSONNode Clone() - { - return new JSONBool(m_Data); - } - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append((m_Data) ? "true" : "false"); - } - public override bool Equals(object obj) - { - if (obj == null) - return false; - if (obj is bool) - return m_Data == (bool)obj; - return false; - } - public override int GetHashCode() - { - return m_Data.GetHashCode(); - } - } - // End of JSONBool - - public partial class JSONNull : JSONNode - { - static JSONNull m_StaticInstance = new JSONNull(); - public static bool reuseSameInstance = true; - public static JSONNull CreateOrGet() - { - if (reuseSameInstance) - return m_StaticInstance; - return new JSONNull(); - } - private JSONNull() { } - - public override JSONNodeType Tag { get { return JSONNodeType.NullValue; } } - public override bool IsNull { get { return true; } } - public override Enumerator GetEnumerator() { return new Enumerator(); } - - public override string Value - { - get { return "null"; } - set { } - } - public override bool AsBool - { - get { return false; } - set { } - } - - public override JSONNode Clone() - { - return CreateOrGet(); - } - - public override bool Equals(object obj) - { - if (object.ReferenceEquals(this, obj)) - return true; - return (obj is JSONNull); - } - public override int GetHashCode() - { - return 0; - } - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append("null"); - } - } - // End of JSONNull - - internal partial class JSONLazyCreator : JSONNode - { - private JSONNode m_Node = null; - private string m_Key = null; - public override JSONNodeType Tag { get { return JSONNodeType.None; } } - public override Enumerator GetEnumerator() { return new Enumerator(); } - - public JSONLazyCreator(JSONNode aNode) - { - m_Node = aNode; - m_Key = null; - } - - public JSONLazyCreator(JSONNode aNode, string aKey) - { - m_Node = aNode; - m_Key = aKey; - } - - private T Set(T aVal) where T : JSONNode - { - if (m_Key == null) - m_Node.Add(aVal); - else - m_Node.Add(m_Key, aVal); - m_Node = null; // Be GC friendly. - return aVal; - } - - public override JSONNode this[int aIndex] - { - get { return new JSONLazyCreator(this); } - set { Set(new JSONArray()).Add(value); } - } - - public override JSONNode this[string aKey] - { - get { return new JSONLazyCreator(this, aKey); } - set { Set(new JSONObject()).Add(aKey, value); } - } - - public override void Add(JSONNode aItem) - { - Set(new JSONArray()).Add(aItem); - } - - public override void Add(string aKey, JSONNode aItem) - { - Set(new JSONObject()).Add(aKey, aItem); - } - - public static bool operator ==(JSONLazyCreator a, object b) - { - if (b == null) - return true; - return System.Object.ReferenceEquals(a, b); - } - - public static bool operator !=(JSONLazyCreator a, object b) - { - return !(a == b); - } - - public override bool Equals(object obj) - { - if (obj == null) - return true; - return System.Object.ReferenceEquals(this, obj); - } - - public override int GetHashCode() - { - return 0; - } - - public override int AsInt - { - get { Set(new JSONNumber(0)); return 0; } - set { Set(new JSONNumber(value)); } - } - - public override float AsFloat - { - get { Set(new JSONNumber(0.0f)); return 0.0f; } - set { Set(new JSONNumber(value)); } - } - - public override double AsDouble - { - get { Set(new JSONNumber(0.0)); return 0.0; } - set { Set(new JSONNumber(value)); } - } - - public override long AsLong - { - get - { - if (longAsString) - Set(new JSONString("0")); - else - Set(new JSONNumber(0.0)); - return 0L; - } - set - { - if (longAsString) - Set(new JSONString(value.ToString())); - else - Set(new JSONNumber(value)); - } - } - - public override bool AsBool - { - get { Set(new JSONBool(false)); return false; } - set { Set(new JSONBool(value)); } - } - - public override JSONArray AsArray - { - get { return Set(new JSONArray()); } - } - - public override JSONObject AsObject - { - get { return Set(new JSONObject()); } - } - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append("null"); - } - } - // End of JSONLazyCreator - - public static class JSON - { - public static JSONNode Parse(string aJSON) - { - return JSONNode.Parse(aJSON); - } - } -} \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON.cs.meta deleted file mode 100644 index 91a8b59..0000000 --- a/Packages/com.adapty.unity-sdk/Runtime/JSON/SimpleJSON.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b15400fbd29f94e9d8efd1c1e6c60ce4 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyConfiguration.Builder.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyConfiguration.Builder.cs index b0bccdb..18f784b 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyConfiguration.Builder.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyConfiguration.Builder.cs @@ -1,15 +1,14 @@ -// -// AdaptyConfiguration.Builder.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 10.12.2024. -// - using System; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyConfiguration + /// + /// Everything + /// needs. Build one with . + /// + [Preserve] + public sealed partial class AdaptyConfiguration { internal AdaptyConfiguration(Builder builder) { @@ -17,42 +16,159 @@ internal AdaptyConfiguration(Builder builder) CustomerUserId = builder.CustomerUserId; CustomerIdentity = builder.CustomerIdentity; ObserverMode = builder.ObserverMode; - AppleIdfaCollectionDisabled = builder.AppleIdfaCollectionDisabled; + // The KidsMode trait compiles IDFA out of the binary; keep the request in sync. + AppleIdfaCollectionDisabled = +#if ADAPTY_KIDS_MODE && UNITY_IOS + true; +#else + builder.AppleIdfaCollectionDisabled; +#endif GoogleAdvertisingIdCollectionDisabled = builder.GoogleAdvertisingIdCollectionDisabled; GoogleEnablePendingPrepaidPlans = builder.GoogleEnablePendingPrepaidPlans; GoogleLocalAccessLevelAllowed = builder.GoogleLocalAccessLevelAllowed; IpAddressCollectionDisabled = builder.IpAddressCollectionDisabled; AppleClearDataOnBackup = builder.AppleClearDataOnBackup; + ServerCluster = builder.ServerCluster; BackendProxyHost = builder.BackendProxyHost; BackendProxyPort = builder.BackendProxyPort; LogLevel = builder.LogLevel; ActivateUI = builder.ActivateUI; AdaptyUIMediaCache = builder.AdaptyUIMediaCache; + + // Not sent when it carries neither value, as Identify does not send it either. + if (CustomerIdentity != null && CustomerIdentity.IsEmpty) + { + CustomerIdentity = null; + } } - public class Builder + /// + /// Assembles an . Every setter returns the builder, so + /// calls chain, and each field can equally well be assigned directly. + /// + /// + /// Only the API key is required, but "not set" does not mean "not sent". The nullable + /// members — , , + /// , , + /// , , + /// and — are left out of + /// the request when null, and the native SDK applies its own default. The rest are not + /// nullable and always go, carrying whatever they hold: a configuration that touches none + /// of them still sends false for both IDFA flags and the IP one, + /// activate_ui: false, log_level: "error" and backend_proxy_port: 0. + /// + public sealed class Builder { + /// + /// The public SDK key from the Adapty Dashboard. Required. + /// public string ApiKey; - public string CustomerUserId; //nullable - public AdaptyCustomerIdentity CustomerIdentity; // nullable + + /// + /// The identifier of the user in your system, when you already know it at activation. + /// Null to stay anonymous and call later. + /// + public string CustomerUserId; + + /// + /// The store account identifiers to attribute purchases with. Null, or an instance + /// carrying neither value, is not sent. + /// + public AdaptyCustomerIdentity CustomerIdentity; + + /// + /// Observer mode: your own code makes the purchases and Adapty only observes them. + /// Null leaves the native default, which is off. + /// public bool? ObserverMode; + + /// + /// iOS only. Stops the SDK collecting the IDFA. Forced on, whatever this says, when + /// the ADAPTY_KIDS_MODE scripting define is set, since the trait compiles IDFA + /// out of the binary. + /// public bool AppleIdfaCollectionDisabled; + + /// + /// Android only. Stops the SDK collecting the Google Advertising ID. + /// public bool GoogleAdvertisingIdCollectionDisabled; + + /// + /// Android only. Reports pending transactions for + /// prepaid plans. + /// public bool GoogleEnablePendingPrepaidPlans; + + /// + /// Android only. + /// Local access levels: + /// when Adapty's servers cannot be reached after a purchase, the SDK verifies it + /// against the store instead and grants the access level on the device. Null leaves + /// the native default, which is off. + /// public bool? GoogleLocalAccessLevelAllowed; + + /// + /// Stops the SDK collecting the device's IP address. + /// public bool IpAddressCollectionDisabled; + + /// + /// iOS only. Clears the SDK's stored data when the app is restored from an iCloud + /// backup, so a restored device does not carry the previous one's profile. Null + /// leaves the native default, which is off. + /// public bool? AppleClearDataOnBackup; - public AdaptyServerCluster ServerCluster; - public string BackendProxyHost; //nullable + + /// + /// Which Adapty server region to talk to. Null uses the default cluster. + /// + public AdaptyServerCluster? ServerCluster; + + /// + /// The host of a proxy to route Adapty's backend calls through. Null for none. + /// + public string BackendProxyHost; + + /// + /// The port of the proxy named by . Ignored without it. + /// public int BackendProxyPort; + + /// + /// How much the native SDK logs. Also settable at any time with + /// . + /// public AdaptyLogLevel LogLevel; + + /// + /// Activates the flow rendering module along with the SDK. Required before + /// can build a view. + /// public bool ActivateUI; - public AdaptyUIMediaCacheConfiguration AdaptyUIMediaCache; //nullable + /// + /// Limits for the cache the flow renderer keeps for images and video. Null leaves the + /// native defaults. + /// + public AdaptyUIMediaCacheConfiguration AdaptyUIMediaCache; + + /// + /// Starts a configuration for the given API key. + /// + /// The public SDK key from the Adapty Dashboard. public Builder(string apiKey) => ApiKey = apiKey; + /// + /// The configuration described by this builder. + /// public AdaptyConfiguration Build() => new AdaptyConfiguration(this); + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(ApiKey)}: {ApiKey}, " + $"{nameof(CustomerUserId)}: {CustomerUserId}, " @@ -60,6 +176,7 @@ public override string ToString() => + $"{nameof(ObserverMode)}: {ObserverMode}, " + $"{nameof(AppleIdfaCollectionDisabled)}: {AppleIdfaCollectionDisabled}, " + $"{nameof(GoogleAdvertisingIdCollectionDisabled)}: {GoogleAdvertisingIdCollectionDisabled}, " + + $"{nameof(GoogleEnablePendingPrepaidPlans)}: {GoogleEnablePendingPrepaidPlans}, " + $"{nameof(GoogleLocalAccessLevelAllowed)}: {GoogleLocalAccessLevelAllowed}, " + $"{nameof(IpAddressCollectionDisabled)}: {IpAddressCollectionDisabled}, " + $"{nameof(AppleClearDataOnBackup)}: {AppleClearDataOnBackup}, " @@ -70,22 +187,42 @@ public override string ToString() => + $"{nameof(AdaptyUIMediaCache)}: {AdaptyUIMediaCache}, " + $"{nameof(LogLevel)}: {LogLevel}"; + /// + /// Replaces the API key the builder was created with. + /// + /// The public SDK key from the Adapty Dashboard. public Builder SetAPIKey(string apiKey) { ApiKey = apiKey; return this; } + /// + /// Sets . + /// + /// The identifier of the user in your system. public Builder SetCustomerUserId(string customerUserId) { CustomerUserId = customerUserId; return this; } + /// + /// Sets together with the store account identifiers to + /// attribute purchases with. An identity carrying neither of them is not sent. + /// + /// The identifier of the user in your system. + /// + /// iOS only. The UUID tying a purchase to its App Store transaction; + /// for none. + /// + /// + /// Android only. The obfuscated account identifier Google Play records; null for none. + /// public Builder SetCustomerUserId( string customerUserId, - Guid iosAppAccountToken, // nullable - string androidObfuscatedAccountId // nullable + Guid iosAppAccountToken, + string androidObfuscatedAccountId ) { CustomerUserId = customerUserId; @@ -96,18 +233,30 @@ string androidObfuscatedAccountId // nullable return this; } + /// + /// Sets . + /// + /// True when your own code makes the purchases and Adapty only observes them. public Builder SetObserverMode(bool observerMode) { ObserverMode = observerMode; return this; } + /// + /// Sets . iOS only. + /// + /// True to stop the SDK collecting the IDFA. public Builder SetAppleIDFACollectionDisabled(bool appleIdfaCollectionDisabled) { AppleIdfaCollectionDisabled = appleIdfaCollectionDisabled; return this; } + /// + /// Sets . Android only. + /// + /// True to stop the SDK collecting the Google Advertising ID. public Builder SetGoogleAdvertisingIdCollectionDisabled( bool googleAdvertisingIdCollectionDisabled ) @@ -116,36 +265,61 @@ bool googleAdvertisingIdCollectionDisabled return this; } + /// + /// Sets . Android only. + /// + /// True to report pending transactions for prepaid plans. public Builder SetGoogleEnablePendingPrepaidPlans(bool googleEnablePendingPrepaidPlans) { GoogleEnablePendingPrepaidPlans = googleEnablePendingPrepaidPlans; return this; } + /// + /// Sets . Android only. + /// + /// True to grant access levels on the device when Adapty cannot be reached. public Builder SetGoogleLocalAccessLevelAllowed(bool googleLocalAccessLevelAllowed) { GoogleLocalAccessLevelAllowed = googleLocalAccessLevelAllowed; return this; } + /// + /// Sets . + /// + /// True to stop the SDK collecting the device's IP address. public Builder SetIPAddressCollectionDisabled(bool ipAddressCollectionDisabled) { IpAddressCollectionDisabled = ipAddressCollectionDisabled; return this; } + /// + /// Sets . iOS only. + /// + /// True to clear stored data when the app is restored from an iCloud backup. public Builder SetAppleClearDataOnBackup(bool appleClearDataOnBackup) { AppleClearDataOnBackup = appleClearDataOnBackup; return this; } + /// + /// Sets . + /// + /// The Adapty server region to talk to. public Builder SetServerCluster(AdaptyServerCluster serverCluster) { ServerCluster = serverCluster; return this; } + /// + /// Sets and . + /// + /// The proxy host to route Adapty's backend calls through. + /// The port on that host. public Builder SetBackendProxy(string host, int port) { BackendProxyHost = host; @@ -153,12 +327,22 @@ public Builder SetBackendProxy(string host, int port) return this; } + /// + /// Sets . + /// + /// True to activate the flow rendering module along with the SDK. public Builder SetActivateUI(bool activate) { ActivateUI = activate; return this; } + /// + /// Sets . Null leaves a native default. + /// + /// In-memory cache limit, in bytes. + /// How many items the in-memory cache holds. + /// On-disk cache limit, in bytes. public Builder SetAdaptyUIMediaCache( int? memoryStorageTotalCostLimit, int? memoryStorageCountLimit, diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyConfiguration.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyConfiguration.cs index 39fc2ff..a658ee2 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyConfiguration.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyConfiguration.cs @@ -1,31 +1,57 @@ -// -// AdaptyConfiguration.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 10.12.2024. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyConfiguration + [DataContract] + public sealed partial class AdaptyConfiguration { + [DataMember(Name = "api_key", IsRequired = true)] private readonly string ApiKey; - private readonly string CustomerUserId; // nullable - private readonly AdaptyCustomerIdentity CustomerIdentity; // nullable + [DataMember(Name = "customer_user_id")] + private readonly string CustomerUserId; + [DataMember(Name = "customer_identity_parameters")] + private readonly AdaptyCustomerIdentity CustomerIdentity; + [DataMember(Name = "observer_mode")] private readonly bool? ObserverMode; + [DataMember(Name = "apple_idfa_collection_disabled")] private readonly bool? AppleIdfaCollectionDisabled; + [DataMember(Name = "google_adid_collection_disabled")] private readonly bool? GoogleAdvertisingIdCollectionDisabled; + [DataMember(Name = "google_enable_pending_prepaid_plans")] private readonly bool? GoogleEnablePendingPrepaidPlans; + [DataMember(Name = "google_local_access_level_allowed")] private readonly bool? GoogleLocalAccessLevelAllowed; + [DataMember(Name = "ip_address_collection_disabled")] private readonly bool? IpAddressCollectionDisabled; + [DataMember(Name = "clear_data_on_backup")] private readonly bool? AppleClearDataOnBackup; + [DataMember(Name = "server_cluster")] private readonly AdaptyServerCluster? ServerCluster; - private readonly string BackendProxyHost; // nullable - private readonly int? BackendProxyPort; // nullable + [DataMember(Name = "backend_proxy_host")] + private readonly string BackendProxyHost; + [DataMember(Name = "backend_proxy_port")] + private readonly int? BackendProxyPort; + [DataMember(Name = "log_level")] private readonly AdaptyLogLevel? LogLevel; + [DataMember(Name = "activate_ui")] private readonly bool? ActivateUI; + [DataMember(Name = "media_cache")] private AdaptyUIMediaCacheConfiguration AdaptyUIMediaCache; + + [DataMember(Name = "cross_platform_sdk_name")] + [Preserve] + private string CrossPlatformSdkName => "unity"; + + [DataMember(Name = "cross_platform_sdk_version")] + [Preserve] + private string CrossPlatformSdkVersion => Adapty.SDKVersion; + + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(ApiKey)}: {ApiKey}, " + $"{nameof(CustomerUserId)}: {CustomerUserId}, " diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyCustomAsset.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyCustomAsset.cs index 7db3df0..b39500b 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyCustomAsset.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyCustomAsset.cs @@ -1,19 +1,16 @@ -// -// AdaptyCustomAsset.cs -// AdaptySDK -// -// Created by Assistant on 14.01.2025. -// - using System; +using System.Collections.Generic; +using System.Runtime.Serialization; using UnityEngine; +using UnityEngine.Scripting; namespace AdaptySDK { /// /// Base class for custom assets that can be used in Adapty UI. /// - public abstract partial class AdaptyCustomAsset + [Preserve] + public abstract class AdaptyCustomAsset { /// /// Creates a custom asset from local image data. @@ -68,6 +65,9 @@ public static AdaptyCustomAsset LocalVideoFile(string path) /// /// Creates a custom asset from a Unity Color. /// + /// + /// Not rendered on iOS: the pinned native SDK substitutes a transparent color for whatever it receives. + /// /// The Unity Color. /// A custom asset representing the color. public static AdaptyCustomAsset Color(Color color) @@ -78,6 +78,9 @@ public static AdaptyCustomAsset Color(Color color) /// /// Creates a custom asset from a Unity Gradient. /// + /// + /// Not rendered on iOS: the pinned native SDK substitutes an empty gradient for whatever it receives. + /// /// The Unity Gradient. /// A custom asset representing the linear gradient. public static AdaptyCustomAsset LinearGradient(Gradient gradient) @@ -89,27 +92,55 @@ public static AdaptyCustomAsset LinearGradient(Gradient gradient) /// /// Custom asset representing local image data. /// - public sealed partial class AdaptyCustomAssetLocalImageData : AdaptyCustomAsset + [DataContract] + [Preserve] + public sealed class AdaptyCustomAssetLocalImageData : AdaptyCustomAsset { + [DataMember(Name = "type", IsRequired = true)] + [Preserve] + private static string Type => "image"; + + [DataMember(Name = "value", IsRequired = true)] + [Preserve] + private byte[] _Data { get; } + /// /// The image data as byte array. /// - public byte[] Data { get; } + /// + /// A copy, as is the array the asset was built from: the request must not change because + /// the caller kept writing into the array it handed over, or into this one. For a large + /// image that is a real copy each way - hold the result if you need it twice. + /// + public byte[] Data => (byte[])_Data.Clone(); internal AdaptyCustomAssetLocalImageData(byte[] data) { - Data = data ?? throw new ArgumentNullException(nameof(data)); + if (data is null) + { + throw new ArgumentNullException(nameof(data)); + } + + _Data = (byte[])data.Clone(); } } /// /// Custom asset representing a local image asset. /// - public sealed partial class AdaptyCustomAssetLocalImageAsset : AdaptyCustomAsset + [DataContract] + [Preserve] + public sealed class AdaptyCustomAssetLocalImageAsset : AdaptyCustomAsset { + [DataMember(Name = "type", IsRequired = true)] + [Preserve] + private static string Type => "image"; + /// /// The asset ID of the image. /// + [DataMember(Name = "asset_id", IsRequired = true)] + [Preserve] public string AssetId { get; } internal AdaptyCustomAssetLocalImageAsset(string assetId) @@ -121,13 +152,23 @@ internal AdaptyCustomAssetLocalImageAsset(string assetId) /// /// Custom asset representing a local image file. /// - public sealed partial class AdaptyCustomAssetLocalImageFile : AdaptyCustomAsset + [DataContract] + [Preserve] + public sealed class AdaptyCustomAssetLocalImageFile : AdaptyCustomAsset { + [DataMember(Name = "type", IsRequired = true)] + [Preserve] + private static string Type => "image"; + /// /// The file path to the image. /// public string Path { get; } + [DataMember(Name = "path", IsRequired = true)] + [Preserve] + private string PathForRequest => AdaptyCustomAssetPath.Resolve(Path); + internal AdaptyCustomAssetLocalImageFile(string path) { Path = path ?? throw new ArgumentNullException(nameof(path)); @@ -137,11 +178,19 @@ internal AdaptyCustomAssetLocalImageFile(string path) /// /// Custom asset representing a local video asset. /// - public sealed partial class AdaptyCustomAssetLocalVideoAsset : AdaptyCustomAsset + [DataContract] + [Preserve] + public sealed class AdaptyCustomAssetLocalVideoAsset : AdaptyCustomAsset { + [DataMember(Name = "type", IsRequired = true)] + [Preserve] + private static string Type => "video"; + /// /// The asset ID of the video. /// + [DataMember(Name = "asset_id", IsRequired = true)] + [Preserve] public string AssetId { get; } internal AdaptyCustomAssetLocalVideoAsset(string assetId) @@ -153,13 +202,23 @@ internal AdaptyCustomAssetLocalVideoAsset(string assetId) /// /// Custom asset representing a local video file. /// - public sealed partial class AdaptyCustomAssetLocalVideoFile : AdaptyCustomAsset + [DataContract] + [Preserve] + public sealed class AdaptyCustomAssetLocalVideoFile : AdaptyCustomAsset { + [DataMember(Name = "type", IsRequired = true)] + [Preserve] + private static string Type => "video"; + /// /// The file path to the video. /// public string Path { get; } + [DataMember(Name = "path", IsRequired = true)] + [Preserve] + private string PathForRequest => AdaptyCustomAssetPath.Resolve(Path); + internal AdaptyCustomAssetLocalVideoFile(string path) { Path = path ?? throw new ArgumentNullException(nameof(path)); @@ -169,13 +228,23 @@ internal AdaptyCustomAssetLocalVideoFile(string path) /// /// Custom asset representing a color. /// - public sealed partial class AdaptyCustomAssetColor : AdaptyCustomAsset + [DataContract] + [Preserve] + public sealed class AdaptyCustomAssetColor : AdaptyCustomAsset { + [DataMember(Name = "type", IsRequired = true)] + [Preserve] + private static string Type => "color"; + /// /// The Unity Color. /// public Color ColorValue { get; } + [DataMember(Name = "value", IsRequired = true)] + [Preserve] + private string ValueForRequest => AdaptyCustomAssetPath.ColorToHex(ColorValue); + internal AdaptyCustomAssetColor(Color color) { ColorValue = color; @@ -185,16 +254,144 @@ internal AdaptyCustomAssetColor(Color color) /// /// Custom asset representing a linear gradient. /// - public sealed partial class AdaptyCustomAssetLinearGradient : AdaptyCustomAsset + [DataContract] + [Preserve] + public sealed class AdaptyCustomAssetLinearGradient : AdaptyCustomAsset { + [DataMember(Name = "type", IsRequired = true)] + [Preserve] + private static string Type => "linear-gradient"; + /// /// The Unity Gradient. /// + /// + /// The request was read off it when the asset was built, so a gradient the caller keeps + /// writing into - this one included - no longer changes what goes out. + /// public Gradient Gradient { get; } + [DataMember(Name = "values", IsRequired = true)] + [Preserve] + private List ValuesForRequest { get; } + + /// + /// A Unity gradient always runs left to right over its full width. + /// + [DataMember(Name = "points", IsRequired = true)] + [Preserve] + private Points PointsForRequest => new Points(); + internal AdaptyCustomAssetLinearGradient(Gradient gradient) { Gradient = gradient ?? throw new ArgumentNullException(nameof(gradient)); + ValuesForRequest = Stops(gradient); + } + + /// + /// Color keys and alpha keys are independent in a Unity Gradient: they may differ in count and sit + /// at different times. Emit a stop at every key time of either channel and let Gradient.Evaluate + /// resolve the RGBA there, so the serialized gradient matches what Unity renders. + /// + private static List Stops(Gradient gradient) + { + var times = new List(); + + foreach (var key in gradient.colorKeys) + { + if (!times.Contains(key.time)) + { + times.Add(key.time); + } + } + + foreach (var key in gradient.alphaKeys) + { + if (!times.Contains(key.time)) + { + times.Add(key.time); + } + } + + times.Sort(); + + var stops = new List(); + foreach (var time in times) + { + stops.Add(new Stop(AdaptyCustomAssetPath.ColorToHex(gradient.Evaluate(time)), time)); + } + + return stops; + } + + [DataContract] + private sealed class Stop + { + internal Stop(string color, double position) + { + Color = color; + Position = position; + } + + [DataMember(Name = "color", IsRequired = true)] + [Preserve] + private string Color { get; } + + [DataMember(Name = "p", IsRequired = true)] + [Preserve] + private double Position { get; } + } + + [DataContract] + private sealed class Points + { + [DataMember(Name = "x0", IsRequired = true)] + [Preserve] + private double X0 => 0.0; + + [DataMember(Name = "y0", IsRequired = true)] + [Preserve] + private double Y0 => 0.0; + + [DataMember(Name = "x1", IsRequired = true)] + [Preserve] + private double X1 => 1.0; + + [DataMember(Name = "y1", IsRequired = true)] + [Preserve] + private double Y1 => 0.0; + } + } + + /// + /// Shared helpers for the write-only custom asset payloads. + /// + [Preserve] + internal static class AdaptyCustomAssetPath + { + /// + /// A path given by the app is relative to StreamingAssets; the native side needs the real + /// location, which differs per platform. + /// + internal static string Resolve(string path) + { +#if UNITY_IOS && !UNITY_EDITOR + return UnityEngine.Application.dataPath + "/Raw/" + path; +#elif UNITY_ANDROID && !UNITY_EDITOR + return "jar:file://" + UnityEngine.Application.dataPath + "!/assets/" + path; +#else + return path; +#endif + } + + internal static string ColorToHex(Color color) + { + var r = Mathf.RoundToInt(color.r * 255); + var g = Mathf.RoundToInt(color.g * 255); + var b = Mathf.RoundToInt(color.b * 255); + var a = Mathf.RoundToInt(color.a * 255); + + return $"#{r:X2}{g:X2}{b:X2}{a:X2}"; } } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyCustomerIdentity.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyCustomerIdentity.cs index f1dffb9..b4595be 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyCustomerIdentity.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyCustomerIdentity.cs @@ -1,18 +1,15 @@ -// -// AdaptyCustomerIdentity.cs -// AdaptySDK -// -// Created by AI Assistant on 14.01.2025. -// - using System; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { /// /// Customer identity parameters for iOS and Android platforms. /// - public partial class AdaptyCustomerIdentity + [DataContract] + [Preserve] + public sealed class AdaptyCustomerIdentity { /// /// The UUID that you generate to associate a customer's In-App Purchase with its resulting App Store transaction. (iOS Only). Nullable. @@ -33,8 +30,8 @@ public partial class AdaptyCustomerIdentity /// /// Initializes a new instance of the AdaptyCustomerIdentity class. /// - /// The UUID for iOS App Store transactions (iOS Only). Nullable. - /// The obfuscated account identifier (Android Only). Nullable. + /// The UUID for iOS App Store transactions (iOS Only). Nullable. + /// The obfuscated account identifier (Android Only). Nullable. public AdaptyCustomerIdentity(Guid iosAppAccountToken, string androidObfuscatedAccountId) { IosAppAccountToken = iosAppAccountToken; @@ -42,10 +39,27 @@ public AdaptyCustomerIdentity(Guid iosAppAccountToken, string androidObfuscatedA } /// - /// Gets a value indicating whether both AppAccountToken and ObfuscatedAccountId are null. + /// Gets a value indicating whether neither AppAccountToken nor ObfuscatedAccountId carries a value. /// - public bool IsEmpty => IosAppAccountToken == null && AndroidObfuscatedAccountId == null; + public bool IsEmpty => + IosAppAccountToken == Guid.Empty && string.IsNullOrEmpty(AndroidObfuscatedAccountId); + + // Emitted through members of their own: the contract omits an unset token or account id + // rather than sending an empty value, and NullValueHandling then drops them. + [DataMember(Name = "app_account_token")] + [Preserve] + private Guid? IosAppAccountTokenForRequest => + IosAppAccountToken == Guid.Empty ? (Guid?)null : IosAppAccountToken; + + [DataMember(Name = "obfuscated_account_id")] + [Preserve] + private string AndroidObfuscatedAccountIdForRequest => + string.IsNullOrEmpty(AndroidObfuscatedAccountId) ? null : AndroidObfuscatedAccountId; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(IosAppAccountToken)}: {IosAppAccountToken}, " + $"{nameof(AndroidObfuscatedAccountId)}: {AndroidObfuscatedAccountId}"; diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyError.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyError.cs index 1c394de..0e7ef33 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyError.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyError.cs @@ -1,25 +1,49 @@ -// -// AdaptyError.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; -namespace AdaptySDK { - public partial class AdaptyError { +namespace AdaptySDK +{ + /// + /// A failure reported by the native SDK. Every completion handler takes one, and it is null + /// when the call succeeded. + /// + [DataContract] + [Preserve] + public sealed class AdaptyError + { + private AdaptyError() { } + + /// + /// What went wrong. Branch on this rather than on , which is not a contract. + /// + [DataMember(Name = "adapty_code", IsRequired = true)] public readonly AdaptyErrorCode Code; + /// + /// A description of the failure, for a log. Not localized and not stable between versions. + /// + [DataMember(Name = "message", IsRequired = true)] public readonly string Message; - public readonly string Detail; // nullable + /// + /// What the native side added about this particular failure — the underlying exception, a store + /// response. Null when there is nothing to add. + /// + [DataMember(Name = "detail")] + public readonly string Detail; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Code)}: {Code}, " + $"{nameof(Message)}: {Message}, " + $"{nameof(Detail)}: {Detail}"; - internal AdaptyError(AdaptyErrorCode Code, string Message, string Detail) { + internal AdaptyError(AdaptyErrorCode Code, string Message, string Detail) + { this.Message = Message; this.Detail = Detail; this.Code = Code; } } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyErrorCode.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyErrorCode.cs index 8c20920..1657505 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyErrorCode.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyErrorCode.cs @@ -1,78 +1,333 @@ -// -// AdaptyErrorCode.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - -namespace AdaptySDK { - public enum AdaptyErrorCode { - // system storekit codes +using UnityEngine.Scripting; + +namespace AdaptySDK +{ + /// + /// The numeric code carried by . + /// + /// + /// Almost every value is the native SDK's own, so a code always arrives whether or not this + /// enum names it. Most of those are produced by one platform only, and each member says which; + /// where both produce a number, both meanings are given, because they are not always the same + /// one. Verified against AdaptySDK-iOS 4.0.2 and AdaptySDK-Android 4.0.1, the pinned + /// dependencies. Two are different: and + /// are also raised by this SDK itself, on either platform, when a + /// request cannot be encoded or a reply cannot be read — the native side is never reached in + /// the first case and has already answered in the second. + /// + [Preserve] + public enum AdaptyErrorCode + { + /// + /// A failure the native SDK could not classify. + /// Unknown = 0, - ClientInvalid = 1, // client is not allowed to issue the request, etc. - PaymentCancelled = 2, // user cancelled the request, etc. - PaymentInvalid = 3, // purchase identifier was invalid, etc. - PaymentNotAllowed = 4, // this device is not allowed to make the payment - StoreProductNotAvailable = 5, // Product is not available in the current storefront - CloudServicePermissionDenied = 6, // user has not allowed access to cloud service information - CloudServiceNetworkConnectionFailed = 7, // the device could not connect to the nework - CloudServiceRevoked = 8, // user has revoked permission to use this cloud service - PrivacyAcknowledgementRequired = 9, // The user needs to acknowledge Apple's privacy policy - UnauthorizedRequestData = 10, // The app is attempting to use SKPayment's requestData property, but does not have the appropriate entitlement - InvalidOfferIdentifier = 11, // The specified subscription offer identifier is not valid - InvalidSignature = 12, // The cryptographic signature provided is not valid - MissingOfferParams = 13, // One or more parameters from SKPaymentDiscount is missing + + /// + /// iOS only. The client is not allowed to make the request. + /// + ClientInvalid = 1, + + /// + /// iOS only. The user cancelled the request. Not a failure to report to them. + /// + PaymentCancelled = 2, + + /// + /// iOS only. The purchase identifier was invalid. + /// + PaymentInvalid = 3, + + /// + /// iOS only. This device is not allowed to make the payment — parental controls, for + /// example. + /// + PaymentNotAllowed = 4, + + /// + /// The product is not available in the current storefront. On Android this is Google + /// Play's ITEM_UNAVAILABLE, which the native SDK maps to this code by name rather + /// than by the offset the other billing codes use. + /// + StoreProductNotAvailable = 5, + + /// + /// iOS only. The user has not allowed access to cloud service information. + /// + CloudServicePermissionDenied = 6, + + /// + /// iOS only. The device could not connect to the network. + /// + CloudServiceNetworkConnectionFailed = 7, + + /// + /// iOS only. The user has revoked permission to use this cloud service. + /// + CloudServiceRevoked = 8, + + /// + /// iOS only. The user needs to acknowledge Apple's privacy policy. + /// + PrivacyAcknowledgementRequired = 9, + + /// + /// iOS only. The app is using SKPayment.requestData without the entitlement for it. + /// + UnauthorizedRequestData = 10, + + /// + /// iOS only. The subscription offer identifier is not valid. + /// + InvalidOfferIdentifier = 11, + + /// + /// iOS only. The cryptographic signature of a promotional offer is not valid. + /// + InvalidSignature = 12, + + /// + /// iOS only. One or more parameters of SKPaymentDiscount is missing. + /// + MissingOfferParams = 13, + + /// + /// iOS only. The price of the offer is not valid. + /// InvalidOfferPrice = 14, - //custom android codes + /// + /// Android only. The SDK was called before . + /// AdaptyNotInitialized = 20, + + /// + /// Android only. The product was not found in Google Play for this application. + /// ProductNotFound = 22, - InvalidJson = 23, + + /// + /// Android only. The subscription being replaced was not found in the purchase history. + /// CurrentSubscriptionToUpdateNotFoundInHistory = 24, - PendingPurchase = 25, + + /// + /// Android only. Google Play's SERVICE_TIMEOUT: the billing service did not answer + /// in time. Worth retrying. + /// BillingServiceTimeout = 97, + + /// + /// Android only. Google Play's FEATURE_NOT_SUPPORTED: the Play Store version on the + /// device does not support what was asked for. + /// FeatureNotSupported = 98, + + /// + /// Android only. Google Play's SERVICE_DISCONNECTED: the connection to the billing + /// service was lost. Worth retrying. + /// BillingServiceDisconnected = 99, + + /// + /// Android only. Google Play's SERVICE_UNAVAILABLE: the billing service is not + /// reachable, usually a network problem. Worth retrying. + /// BillingServiceUnavailable = 102, + + /// + /// Android only. Google Play's BILLING_UNAVAILABLE: billing is unavailable for this + /// user or this API version — an unsupported Play Store, or a user who cannot transact. + /// BillingUnavailable = 103, + + /// + /// Android only. Google Play's DEVELOPER_ERROR: the request was malformed. A + /// configuration problem in the app or the Play Console, not something the user can act on. + /// DeveloperError = 105, + + /// + /// Android only. Google Play's ERROR, and the fallback for any billing response the + /// native SDK does not name. + /// BillingError = 106, + + /// + /// Android only. Google Play's ITEM_ALREADY_OWNED: the user already owns this + /// product. Restore rather than buy. + /// ItemAlreadyOwned = 107, + + /// + /// Android only. Google Play's ITEM_NOT_OWNED: the product being consumed or + /// replaced is not owned by the user. + /// ItemNotOwned = 108, - // custom storekit codes - NoProductIDsFound = 1000, // No In-App Purchase product identifiers were found - ProductRequestFailed = 1002, // Unable to fetch available In-App Purchase products at the moment - CantMakePayments = 1003, // In-App Purchases are not allowed on this device - // NoPurchasesToRestore = 1004, // No purchases to restore - CantReadReceipt = 1005, // Can't find a valid receipt - ProductPurchaseFailed = 1006, // Product purchase failed + /// + /// Android only. Google Play's NETWORK_ERROR: the request to the billing service + /// failed on the network. Worth retrying. + /// + BillingNetworkError = 112, + + /// + /// No products were found for the placement. Usually a Dashboard or store configuration + /// that has not propagated yet. + /// + NoProductIDsFound = 1000, + + /// + /// iOS only. The App Store could not be asked for the products. + /// + ProductRequestFailed = 1002, + + /// + /// iOS only. In-app purchases are not allowed on this device. + /// + CantMakePayments = 1003, + + /// + /// Android only. found nothing to restore. iOS does + /// not produce this code. + /// + NoPurchasesToRestore = 1004, + + /// + /// iOS only. No valid App Store receipt was found on the device. + /// + CantReadReceipt = 1005, + + /// + /// iOS only. The purchase failed in StoreKit. + /// + ProductPurchaseFailed = 1006, + + /// + /// iOS only. Refreshing the App Store receipt failed. + /// RefreshReceiptFailed = 1010, + + /// + /// iOS only. The subscription status could not be fetched from the App Store. + /// FetchSubscriptionStatusFailed = 1020, - // custom network codes - NotActivated = 2002, // You need to be authenticated first - BadRequest = 2003, // Bad request - ServerError = 2004, // Response code is 429 or 500s - NetworkFailed = 2005, // Network request failed - DecodingFailed = 2006, // We could not decode the response - EncodingFailed = 2009, // Parameters encoding failed - AnalyticsDisabled = 3000, // Request url is nil + /// + /// iOS only, and not reachable from Unity today. The native SDK raises it when StoreKit + /// answers a purchase with .pending — Ask to Buy, or a payment method that settles + /// later — from an overload that takes StoreKit's own result. The Unity bridge reports a + /// transaction by its id, which is a different path with no pending state, so the code is + /// named here for completeness rather than as something to handle. A pending purchase made + /// through the SDK arrives as instead. + /// + PaymentPendingError = 1050, + + /// + /// The two platforms mean different things by this number. On iOS the SDK was called + /// before ; on Android the Adapty backend answered 401 or 403, + /// which points at the API key. + /// + NotActivated = 2002, + + /// + /// The Adapty backend answered with a 4xx other than 401 and 403. + /// + BadRequest = 2003, + + /// + /// The Adapty backend answered 429, 499 or a 5xx. Worth retrying. + /// + ServerError = 2004, - /// Wrong parameter was passed. + /// + /// The request to the Adapty backend failed on the network. + /// + NetworkFailed = 2005, + + /// + /// A response could not be decoded. If it arrives from a call this SDK makes, the versions + /// of the Unity and native SDKs may not match. + /// + /// + /// Raised by this SDK on either platform, as well as by the native ones. + /// + DecodingFailed = 2006, + + /// + /// The parameters of a request could not be encoded, so the call never left this SDK. + /// + /// + /// Raised on either platform, not only iOS: the request is built and encoded in managed + /// code, before the native bridge is reached. A value the serializer cannot write is what + /// produces it — a reference loop or a throwing getter in something the app passed in. + /// The native iOS SDK declares the same number for its own encoding failures. + /// + EncodingFailed = 2009, + + /// + /// The call needs analytics, which the profile has switched off. + /// + AnalyticsDisabled = 3000, + + /// + /// A parameter of the call was not valid. + /// WrongParam = 3001, - /// It is not possible to call `.activate` method more than once. + /// + /// iOS only. was called more than once. + /// ActivateOnceError = 3005, - /// The user profile was changed during the operation. + /// + /// The profile changed while the operation was running — an + /// or in between. Repeat the operation on the new profile. + /// ProfileWasChanged = 3006, + + /// + /// iOS only. The data handed to the SDK is of a shape it does not support. + /// UnsupportedData = 3007, + + /// + /// was called for a profile that was never identified. + /// + UnidentifiedUserLogout = 3020, + + /// + /// iOS only. The fetch did not finish within the timeout the call was given. + /// FetchTimeoutError = 3101, - OperationInterrupted = 9000 - /// Plugin errors - // WrongCallParameter = 10001 - } + /// + /// Android only. Reported through FlowViewDidReceiveError when an asset in the flow + /// is not of the type the layout expects. + /// + WrongAssetType = 4104, + + /// + /// Android only. Reported through FlowViewDidReceiveError when the flow's web view + /// raised a JavaScript exception. + /// + JsException = 4105, -} \ No newline at end of file + /// + /// Android only. Reported through FlowViewDidReceiveError when the flow asked to + /// navigate somewhere the renderer has no navigator for. + /// + NavigatorNotFound = 4106, + + /// + /// Android only. Reported through FlowViewDidReceiveError when an action in the flow + /// carries a URL that cannot be opened. + /// + InvalidActionUrl = 4107, + + /// + /// iOS only. The operation was interrupted before it could finish. + /// + OperationInterrupted = 9000, + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlow.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlow.cs index 83a2b30..b3e3e6e 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlow.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlow.cs @@ -1,9 +1,7 @@ -// -// AdaptyFlow.cs -// AdaptySDK -// - using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { @@ -14,26 +12,34 @@ namespace AdaptySDK /// A flow is a set of paywall variations that can be displayed to users. It contains information about the placement, paywalls, and remote configs. /// Read more at Adapty Documentation /// - public partial class AdaptyFlow + [DataContract] + [Preserve] + public sealed class AdaptyFlow { + private AdaptyFlow() => Freeze(); + /// /// An object that contains information about the placement of the flow. /// + [DataMember(Name = "placement", IsRequired = true)] public readonly AdaptyPlacement Placement; /// /// A unique identifier for this flow instance. /// + [DataMember(Name = "flow_id", IsRequired = true)] public readonly string InstanceIdentity; /// /// The flow name configured in the Adapty Dashboard. /// + [DataMember(Name = "flow_name", IsRequired = true)] public readonly string Name; /// /// The identifier of the variation, used to attribute purchases to the flow. /// + [DataMember(Name = "variation_id", IsRequired = true)] public readonly string VariationId; /// @@ -42,12 +48,21 @@ public partial class AdaptyFlow /// /// This can be null if the version identifier is not available. /// - public readonly string FlowVersionId; // nullable + [DataMember(Name = "flow_version_id")] + public readonly string FlowVersionId; /// /// Array of custom JSON formatted data configured in the Adapty Dashboard, one entry per locale. /// - public readonly IList RemoteConfigs; + [DataMember(Name = "remote_configs")] + private readonly List _RemoteConfigs = new List(); + + /// + /// The remote configs of the flow, one per localization. Empty when none is configured; + /// is the first of them. + /// + [Preserve] + public IReadOnlyList RemoteConfigs { get; private set; } /// /// The first custom JSON formatted data configured in the Adapty Dashboard. @@ -63,15 +78,37 @@ public AdaptyRemoteConfig RemoteConfig /// /// Array of paywall variations associated with this flow. /// - public readonly IList Paywalls; + [DataMember(Name = "variations", IsRequired = true)] + private readonly List _Paywalls; + + /// + /// The paywall variations this flow offers. + /// + [Preserve] + public IReadOnlyList Paywalls { get; private set; } + [DataMember(Name = "response_created_at", IsRequired = true)] private readonly long _ResponseCreatedAt; - private readonly string _PayloadData; // nullable + [DataMember(Name = "payload_data")] + private readonly string _PayloadData; + + [Preserve] + [OnDeserialized] + private void OnDeserialized(StreamingContext context) => Freeze(); + + private void Freeze() + { + RemoteConfigs = new ReadOnlyCollection(_RemoteConfigs); + Paywalls = + _Paywalls is null + ? null + : new ReadOnlyCollection(_Paywalls); + } /// /// Array of vendor product IDs (App Store or Google Play product identifiers) aggregated across all paywall variations of this flow. /// - public IList VendorProductIds + public IReadOnlyList VendorProductIds { get { @@ -87,14 +124,14 @@ public IList VendorProductIds } } } - return list; + return new ReadOnlyCollection(list); } } /// /// Array of product identifiers aggregated across all paywall variations of this flow. /// - public IList ProductIdentifiers + public IReadOnlyList ProductIdentifiers { get { @@ -110,10 +147,14 @@ public IList ProductIdentifiers } } } - return list; + return new ReadOnlyCollection(list); } } + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Placement)}: {Placement}, " + $"{nameof(InstanceIdentity)}: {InstanceIdentity}, " diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlowPaywall.ProductReference.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlowPaywall.ProductReference.cs index 74b2e22..f780d19 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlowPaywall.ProductReference.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlowPaywall.ProductReference.cs @@ -1,25 +1,44 @@ -// -// AdaptyFlowPaywall.ProductReference.cs -// AdaptySDK -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyFlowPaywall + [Preserve] + public sealed partial class AdaptyFlowPaywall { - public partial class ProductReference + [DataContract] + internal class ProductReference { - internal readonly string FlowProductId; //nullable + private ProductReference() { } + + [DataMember(Name = "flow_product_id")] + internal readonly string FlowProductId; + [DataMember(Name = "vendor_product_id", IsRequired = true)] internal readonly string VendorProductId; + [DataMember(Name = "adapty_product_id", IsRequired = true)] internal readonly string AdaptyProductId; + [DataMember(Name = "access_level_id", IsRequired = true)] internal readonly string AccessLevelId; + [DataMember(Name = "product_type", IsRequired = true)] internal readonly string ProductType; - internal readonly string PromotionalOfferId; //nullable - internal readonly string WinBackOfferId; //nullable - internal readonly string AndroidBasePlanId; //nullable - internal readonly string AndroidOfferId; //nullable +#if UNITY_IOS + [DataMember(Name = "promotional_offer_id")] +#endif + internal readonly string PromotionalOfferId; +#if UNITY_IOS + [DataMember(Name = "win_back_offer_id")] +#endif + internal readonly string WinBackOfferId; +#if UNITY_ANDROID + [DataMember(Name = "base_plan_id")] +#endif + internal readonly string AndroidBasePlanId; +#if UNITY_ANDROID + [DataMember(Name = "offer_id")] +#endif + internal readonly string AndroidOfferId; - public AdaptyProductIdentifier ToAdaptyProductIdentifier() + internal AdaptyProductIdentifier ToAdaptyProductIdentifier() { return new AdaptyProductIdentifier( vendorProductId: VendorProductId, @@ -28,6 +47,10 @@ public AdaptyProductIdentifier ToAdaptyProductIdentifier() ); } + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(FlowProductId)}: {FlowProductId}, " + $"{nameof(VendorProductId)}: {VendorProductId}, " diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlowPaywall.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlowPaywall.cs index eeda391..73a6039 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlowPaywall.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyFlowPaywall.cs @@ -1,9 +1,7 @@ -// -// AdaptyFlowPaywall.cs -// AdaptySDK -// - using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { @@ -14,35 +12,44 @@ namespace AdaptySDK /// A flow paywall is a set of products that can be displayed to users within a flow. /// Read more at Adapty Documentation /// - public partial class AdaptyFlowPaywall + [DataContract] + public sealed partial class AdaptyFlowPaywall { + private AdaptyFlowPaywall() { } + /// /// An object that contains information about the placement of the paywall. /// + [DataMember(Name = "placement", IsRequired = true)] public readonly AdaptyPlacement Placement; /// /// A unique identifier for this paywall instance. /// + [DataMember(Name = "paywall_id", IsRequired = true)] public readonly string InstanceIdentity; /// /// The paywall name configured in the Adapty Dashboard. /// + [DataMember(Name = "paywall_name", IsRequired = true)] public readonly string Name; /// /// The identifier of the variation, used to attribute purchases to the paywall. /// + [DataMember(Name = "variation_id", IsRequired = true)] public readonly string VariationId; + [DataMember(Name = "products", IsRequired = true)] internal readonly IList _Products; - private readonly string _WebPurchaseUrl; // nullable + [DataMember(Name = "web_purchase_url")] + private readonly string _WebPurchaseUrl; /// /// Array of vendor product IDs (App Store or Google Play product identifiers) associated with this paywall. /// - public IList VendorProductIds + public IReadOnlyList VendorProductIds { get { @@ -51,14 +58,14 @@ public IList VendorProductIds { list.Add(item.VendorProductId); } - return list; + return new ReadOnlyCollection(list); } } /// /// Array of product identifiers associated with this paywall. /// - public IList ProductIdentifiers + public IReadOnlyList ProductIdentifiers { get { @@ -67,10 +74,14 @@ public IList ProductIdentifiers { list.Add(product.ToAdaptyProductIdentifier()); } - return list; + return new ReadOnlyCollection(list); } } + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Placement)}: {Placement}, " + $"{nameof(InstanceIdentity)}: {InstanceIdentity}, " diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationDetails.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationDetails.cs index 9bd4cea..46d14e6 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationDetails.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationDetails.cs @@ -1,21 +1,45 @@ -// -// AdaptyInstallationDetails.cs -// AdaptySDK -// -// Created by Alexey Goncharov on 10.09.2025. -// - using System; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyInstallationDetails + /// + /// What the SDK knows about this installation of the app. + /// + [DataContract] + [Preserve] + public sealed class AdaptyInstallationDetails { - public readonly string InstallId; // nullable - public readonly DateTime InstallTime; // Date string, non-null - public readonly int AppLaunchCount; // non-null - public readonly string Payload; // nullable + private AdaptyInstallationDetails() { } + + /// + /// Adapty's identifier for this installation, from the registration it performs. Null when it + /// has not been established. + /// + [DataMember(Name = "install_id")] + public readonly string InstallId; + /// + /// When the app was installed, on the machine's clock — the wire carries UTC and the SDK converts. + /// + [DataMember(Name = "install_time", IsRequired = true)] + public readonly DateTime InstallTime; + /// + /// How many times the app has been launched, counted by the SDK. + /// + [DataMember(Name = "app_launch_count", IsRequired = true)] + public readonly int AppLaunchCount; + /// + /// The install payload the attribution provider passed through, as its own string. Null when there + /// was none. + /// + [DataMember(Name = "payload")] + public readonly string Payload; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"(installId: {InstallId}, " + $"installTime: {InstallTime}, " diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationStatus.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationStatus.cs index 3e2c329..964826a 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationStatus.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationStatus.cs @@ -1,41 +1,56 @@ -// -// AdaptyInstallationStatus.cs -// AdaptySDK -// -// Created by Alexey Goncharov on 10.09.2025. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public abstract class AdaptyInstallationStatus + /// + /// What reports: how much is known about this + /// installation, and the details when they are. + /// + [DataContract] + [Preserve] + public sealed class AdaptyInstallationStatus { - internal AdaptyInstallationStatus() { } - } - - public sealed class AdaptyInstallationStatusNotAvailable : AdaptyInstallationStatus - { - public AdaptyInstallationStatusNotAvailable() { } - - public override string ToString() => nameof(AdaptyInstallationStatusNotAvailable); - } - - public sealed class AdaptyInstallationStatusNotDetermined : AdaptyInstallationStatus - { - public AdaptyInstallationStatusNotDetermined() { } - - public override string ToString() => nameof(AdaptyInstallationStatusNotDetermined); - } - - public sealed class AdaptyInstallationStatusDetermined : AdaptyInstallationStatus - { - public readonly AdaptyInstallationDetails Details; - - public AdaptyInstallationStatusDetermined(AdaptyInstallationDetails details) + private AdaptyInstallationStatus() { } + + /// + /// How much is known. is set when this is + /// . + /// + [DataMember(Name = "status", IsRequired = true)] + public readonly AdaptyInstallationStatusType Status; + + /// + /// The installation, present when is + /// and null otherwise. + /// + [DataMember(Name = "details")] + [Preserve] + public AdaptyInstallationDetails Details { get; private set; } + + // The contract carries details on the determined branch only, which no attribute can say. + // Dropping it elsewhere rather than failing is what the branch-per-subclass model did. + // [Preserve] because a type's attribute does not cover its methods. + [Preserve] + [OnDeserialized] + private void OnDeserialized(StreamingContext context) { - Details = details; + if (Status != AdaptyInstallationStatusType.Determined) + { + Details = null; + } + else if (Details is null) + { + throw Serialization.AdaptyJsonRequire.Missing("details"); + } } + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => - $"{nameof(AdaptyInstallationStatusDetermined)}({Details})"; + $"{nameof(Status)}: {Status}, " + + $"{nameof(Details)}: {(Details == null ? "null" : Details.ToString())}"; } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationStatusType.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationStatusType.cs new file mode 100644 index 0000000..66506c7 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationStatusType.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; +using UnityEngine.Scripting; + +namespace AdaptySDK +{ + /// + /// How much the SDK knows about this installation. + /// + [Preserve] + public enum AdaptyInstallationStatusType + { + /// + /// The details are not available. Reported when the platform has nothing to report, and on iOS + /// also when the install time or the launch count could not be obtained. + /// + [EnumMember(Value = "not_available")] + NotAvailable = 0, + + /// + /// Not established yet. Ask again later. + /// + [EnumMember(Value = "not_determined")] + NotDetermined = 1, + + /// + /// Established — the details are on . + /// + [EnumMember(Value = "determined")] + Determined = 2, + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationStatusType.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationStatusType.cs.meta new file mode 100644 index 0000000..f1f75fb --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyInstallationStatusType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c3305df233b54364bb2cab502d24c761 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyLogLevel.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyLogLevel.cs index 990316a..71d69b1 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyLogLevel.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyLogLevel.cs @@ -1,18 +1,41 @@ -// -// AdaptyLogLevel.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// + /// How much the native SDK writes to the platform log. + /// + /// + /// Each level includes the ones before it. Set it in the configuration, or at any time with . + /// + [Preserve] public enum AdaptyLogLevel { - Error, - Warn, - Info, - Verbose, - Debug + /// + /// Failures only. + /// + [EnumMember(Value = "error")] + Error = 0, + /// + /// Failures, and conditions the SDK could work around. + /// + [EnumMember(Value = "warn")] + Warn = 1, + /// + /// The above, plus the significant things the SDK does. + /// + [EnumMember(Value = "info")] + Info = 2, + /// + /// The above, plus the calls made and the requests sent. + /// + [EnumMember(Value = "verbose")] + Verbose = 3, + /// + /// Everything, including payload bodies. For development, not for a shipped build. + /// + [EnumMember(Value = "debug")] + Debug = 4 } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPaymentMode.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPaymentMode.cs index b51aab0..c5ed1eb 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPaymentMode.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPaymentMode.cs @@ -1,17 +1,33 @@ -// -// AdaptyPaymentMode.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// + /// How a discounted subscription phase is paid for. + /// + [Preserve] public enum AdaptyPaymentMode { - PayAsYouGo, - PayUpFront, - FreeTrial, - Unknown, + /// + /// Reduced price, charged each period of the offer. + /// + [EnumMember(Value = "pay_as_you_go")] + PayAsYouGo = 0, + /// + /// The whole offer paid once, at its start. + /// + [EnumMember(Value = "pay_up_front")] + PayUpFront = 1, + /// + /// Nothing is charged for the phase. + /// + [EnumMember(Value = "free_trial")] + FreeTrial = 2, + /// + /// The store reported a mode the contract does not list. One of the two enums that keep an unknown value, because the contract lists "unknown" among theirs. + /// + [EnumMember(Value = "unknown")] + Unknown = 3, } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPaywallProduct.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPaywallProduct.cs index ce2068d..20607f6 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPaywallProduct.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPaywallProduct.cs @@ -1,9 +1,5 @@ -// -// AdaptyPaywallProduct.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { @@ -14,16 +10,22 @@ namespace AdaptySDK /// This class contains all information about a product including pricing, subscription details, and metadata. /// Read more at Adapty Documentation /// - public partial class AdaptyPaywallProduct + [DataContract] + [Preserve] + public sealed class AdaptyPaywallProduct { + private AdaptyPaywallProduct() { } + /// /// The unique identifier of the product in the App Store or Google Play Store. /// + [DataMember(Name = "vendor_product_id", IsRequired = true)] public readonly string VendorProductId; /// /// The unique identifier of the product in Adapty. /// + [DataMember(Name = "adapty_product_id", IsRequired = true)] public readonly string AdaptyProductId; /// @@ -32,7 +34,8 @@ public partial class AdaptyPaywallProduct /// /// This can be null if the product does not belong to a flow. /// - public readonly string FlowProductId; //nullable + [DataMember(Name = "flow_product_id")] + public readonly string FlowProductId; /// /// The identifier of the access level configured in the Adapty Dashboard. @@ -40,41 +43,51 @@ public partial class AdaptyPaywallProduct /// /// When a user purchases this product, they will be granted access to this access level. /// + [DataMember(Name = "access_level_id", IsRequired = true)] public readonly string AccessLevelId; /// /// The type of the product (e.g., "consumable", "non_consumable", "subscription"). /// + [DataMember(Name = "product_type", IsRequired = true)] public readonly string ProductType; /// /// The identifier of the variation, used to attribute purchases to the paywall. /// + [DataMember(Name = "paywall_variation_id", IsRequired = true)] public readonly string PaywallVariationId; /// /// The parent A/B test name associated with this product. /// + [DataMember(Name = "paywall_ab_test_name", IsRequired = true)] public readonly string PaywallABTestName; /// /// The parent paywall name associated with this product. /// + [DataMember(Name = "paywall_name", IsRequired = true)] public readonly string PaywallName; /// /// A localized description of the product. /// + [DataMember(Name = "localized_description", IsRequired = true)] public readonly string LocalizedDescription; /// /// The localized name of the product. /// + [DataMember(Name = "localized_title", IsRequired = true)] public readonly string LocalizedTitle; /// /// Indicates whether the product is available for family sharing in App Store Connect (iOS only). /// +#if UNITY_IOS + [DataMember(Name = "is_family_shareable", IsRequired = true)] +#endif public readonly bool IsFamilyShareable; /// @@ -83,11 +96,13 @@ public partial class AdaptyPaywallProduct /// /// This can be null if the region code is not available. /// + [DataMember(Name = "region_code")] public readonly string RegionCode; /// /// The object that represents the main price for the product. /// + [DataMember(Name = "price", IsRequired = true)] public readonly AdaptyPrice Price; /// @@ -96,16 +111,27 @@ public partial class AdaptyPaywallProduct /// /// This is null for non-subscription products. /// - public readonly AdaptySubscription Subscription; //nullable + [DataMember(Name = "subscription")] + public readonly AdaptySubscription Subscription; /// /// The index of the product in the paywall (0-based). /// + [DataMember(Name = "paywall_product_index", IsRequired = true)] public readonly int PaywallProductIndex; + [DataMember(Name = "payload_data")] private readonly string _PayloadData; + [DataMember(Name = "web_purchase_url")] private readonly string _WebPurchaseUrl; + internal string PayloadData => _PayloadData; + internal string WebPurchaseUrl => _WebPurchaseUrl; + + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(VendorProductId)}: {VendorProductId}, " + $"{nameof(AdaptyProductId)}: {AdaptyProductId}, " diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPlacement.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPlacement.cs index b508729..9a2e45f 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPlacement.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPlacement.cs @@ -1,37 +1,65 @@ -// -// AdaptyPlacement.cs -// AdaptySDK -// -// Created by Aleksei Goncharov on 09.09.2025. -// - using System.Collections.Generic; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - using AdaptySDK.SimpleJSON; - public partial class AdaptyPlacement + /// + /// The placement a flow was fetched for, with the A/B test and audience it resolved to. + /// + [DataContract] + [Preserve] + public sealed class AdaptyPlacement { + private AdaptyPlacement() { } + /// The identifier of the placement, configured in Adapty Dashboard. + /// + /// The placement identifier from the Dashboard. + /// + [DataMember(Name = "developer_id", IsRequired = true)] public readonly string Id; /// The name of the audience for the placement. + /// + /// The audience the profile fell into. + /// + [DataMember(Name = "audience_name", IsRequired = true)] public readonly string AudienceName; /// The current revision (version) of the placement. + /// + /// Which revision of the placement this is — it goes up on every change in the Dashboard. + /// + [DataMember(Name = "revision", IsRequired = true)] public readonly long Revision; /// Placement A/B test name + /// + /// The A/B test the placement is running, when it is running one. + /// + [DataMember(Name = "ab_test_name", IsRequired = true)] public readonly string ABTestName; /// Placement audience version id + /// + /// The exact placement-and-audience version this was resolved from. + /// + [DataMember(Name = "placement_audience_version_id", IsRequired = true)] public readonly string PlacementAudienceVersionId; - public readonly bool? IsTrackingPurchases; - - public bool GetIsTrackingPurchases => IsTrackingPurchases ?? false; + /// + /// Whether purchases in this placement count towards its analytics. Never arrives null — a + /// missing key leaves the declared false. + /// + [DataMember(Name = "is_tracking_purchases")] + public readonly bool? IsTrackingPurchases = false; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Id)}: {Id}, " + $"{nameof(AudienceName)}: {AudienceName}, " diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPlacementFetchPolicy.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPlacementFetchPolicy.cs index f450ae3..fff8481 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPlacementFetchPolicy.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPlacementFetchPolicy.cs @@ -1,39 +1,70 @@ -// -// AdaptyPlacementFetchPolicy.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 26.12.2023. -// +using System; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - using System; - - public partial class AdaptyPlacementFetchPolicy + /// + /// Whether a fetch may answer from the cache, and for how long. Pick one of the shared + /// instances or build one with . + /// + [DataContract] + [Preserve] + public sealed class AdaptyPlacementFetchPolicy { + [DataMember(Name = "type", IsRequired = true)] private readonly string _Type; + private readonly TimeSpan? _MaxAge; + /// + /// The contract carries the age in seconds, not as a duration literal. + /// + [DataMember(Name = "max_age")] + [Preserve] + private double? MaxAgeInSeconds => _MaxAge?.TotalSeconds; + private AdaptyPlacementFetchPolicy(string type, TimeSpan? maxAge) { _Type = type; _MaxAge = maxAge; } - public static AdaptyPlacementFetchPolicy Default = ReloadRevalidatingCacheData; - public static AdaptyPlacementFetchPolicy ReloadRevalidatingCacheData = new( + /// + /// Ask the server, and fall back to the cache when it cannot be reached. The default. + /// + public static readonly AdaptyPlacementFetchPolicy ReloadRevalidatingCacheData = new( "reload_revalidating_cache_data", null ); - public static AdaptyPlacementFetchPolicy ReturnCacheDataElseLoad = new( + /// + /// Use the cache when there is anything in it, however old, and only ask the server otherwise. + /// + public static readonly AdaptyPlacementFetchPolicy ReturnCacheDataElseLoad = new( "return_cache_data_else_load", null ); + // Declared after the policy it aliases: a static field initializer runs in declaration + // order, so the other way round leaves Default null. + /// + /// The policy used when none is given — the same instance as + /// . + /// + public static readonly AdaptyPlacementFetchPolicy Default = ReloadRevalidatingCacheData; + + /// + /// Use the cache while it is younger than , and ask the server once it + /// is older. + /// public static AdaptyPlacementFetchPolicy ReturnCacheDataIfNotExpiredElseLoad( TimeSpan maxAge ) => new("return_cache_data_if_not_expired_else_load", maxAge); + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(_Type)}: {_Type}, " + $"{nameof(_MaxAge)}: {_MaxAge}"; } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPrice.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPrice.cs index cc86162..7a2aafa 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPrice.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPrice.cs @@ -1,37 +1,56 @@ -// -// AdaptyPrice.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 08.09.2023. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyPrice + /// + /// A price as the store reports it: the amount, and the strings to show it with. + /// + [DataContract] + [Preserve] + public sealed class AdaptyPrice { + private AdaptyPrice() { } + + /// /// Discount price of a product in a local currency. + /// + [DataMember(Name = "amount", IsRequired = true)] public readonly double Amount; + /// /// The currency code of the locale used to format the price of the product. /// /// /// [Nullable] + /// + [DataMember(Name = "currency_code")] public readonly string CurrencyCode; + /// /// The currency symbol of the locale used to format the price of the product. /// /// /// [Nullable] + /// + [DataMember(Name = "currency_symbol")] public readonly string CurrencySymbol; + /// /// A formatted price of a discount for a user's locale. /// /// [Nullable] + /// + [DataMember(Name = "localized_string")] public readonly string LocalizedString; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Amount)}: {Amount}, " + $"{nameof(CurrencyCode)}: {CurrencyCode}, " + $"{nameof(CurrencySymbol)}: {CurrencySymbol}, " + $"{nameof(LocalizedString)}: {LocalizedString}"; } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProductIdentifier.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProductIdentifier.cs index d338d66..0483d60 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProductIdentifier.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProductIdentifier.cs @@ -1,19 +1,45 @@ -// -// AdaptyProductIdentifier.cs -// AdaptySDK -// -// Created by Alexey Goncharov on 10.09.2025. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// /// A lightweight identifier used when addressing a specific product across platforms. - public partial class AdaptyProductIdentifier + /// + [DataContract] + [Preserve] + public sealed class AdaptyProductIdentifier { + private AdaptyProductIdentifier() { } + + /// + /// The product id in App Store Connect or the Google Play Console. + /// + [DataMember(Name = "vendor_product_id", IsRequired = true)] public readonly string VendorProductId; + /// + /// The product's id in Adapty, which is also the key a request addresses it by. + /// + [DataMember(Name = "adapty_product_id", IsRequired = true)] internal readonly string _AdaptyProductId; - public readonly string BasePlanId; // Android Only, nullable + /// + /// Android only. The Google Play base plan. Null on iOS. + /// + /// + /// Empty is the same as none: the contract leaves the key out rather than sending it empty, + /// so the constructor normalizes it and NullValueHandling drops it. + /// + [DataMember(Name = "base_plan_id")] + public readonly string BasePlanId; + /// + /// Builds an identifier for a product you name yourself, rather than one taken from a flow. + /// + /// The product id in App Store Connect or the Google Play Console. + /// + /// The product's id in Adapty, as carries it. + /// + /// Android only. The Google Play base plan, or null for none. public AdaptyProductIdentifier( string vendorProductId, string adaptyProductId, @@ -22,12 +48,15 @@ string basePlanId { VendorProductId = vendorProductId; _AdaptyProductId = adaptyProductId; - BasePlanId = basePlanId; + BasePlanId = string.IsNullOrEmpty(basePlanId) ? null : basePlanId; } + /// + /// Two identifiers are equal when all three of their values are. + /// /// /// Value equality, so an identifier can be used as a dictionary key — for example in - /// , + /// , /// where the caller builds the keys from a flow rather than reusing the SDK's instances. /// public override bool Equals(object obj) @@ -43,6 +72,10 @@ public override bool Equals(object obj) && BasePlanId == other.BasePlanId; } + /// + /// Hashes the three values compares, so an identifier works as a dictionary + /// key. + /// public override int GetHashCode() { var hash = 17; @@ -52,6 +85,10 @@ public override int GetHashCode() return hash; } + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() { return nameof(VendorProductId) diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.AccessLevel.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.AccessLevel.cs index 7c5a538..eec2a53 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.AccessLevel.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.AccessLevel.cs @@ -1,34 +1,37 @@ -// -// AdaptyProfile.AccessLevel.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - using System; +using System.Runtime.Serialization; namespace AdaptySDK { - public partial class AdaptyProfile + public sealed partial class AdaptyProfile { - public partial class AccessLevel + /// + /// One access level of a profile: whether it is active, what granted it, and until when. + /// + [DataContract] + public sealed class AccessLevel { + private AccessLevel() { } + /// /// Unique identifier of the access level configured by you in Adapty Dashboard. /// + [DataMember(Name = "id", IsRequired = true)] public readonly string Id; /// /// Whether the access level is active. /// /// - /// Generally, you have to check just this property to determine if the user has access to premium features. + /// Generally, you have to check just this property to determine if the user has access to premium features. /// + [DataMember(Name = "is_active", IsRequired = true)] public readonly bool IsActive; /// - /// The identifier of the product in the App Store Connect that unlocked this access level. + /// The identifier of the product, in the store it was bought from, that unlocked this access level. /// + [DataMember(Name = "vendor_product_id", IsRequired = true)] public readonly string VendorProductId; /// @@ -37,39 +40,45 @@ public partial class AccessLevel /// /// The possible values are: app_store, play_store, adapty. /// + [DataMember(Name = "store", IsRequired = true)] public readonly string Store; /// /// The time when the access level was activated. /// + [DataMember(Name = "activated_at", IsRequired = true)] public readonly DateTime ActivatedAt; /// - /// The time when the access level was renewed. + /// The time when the access level was renewed. Null when there has been no renewal. /// - public readonly DateTime? RenewedAt; // nullable + [DataMember(Name = "renewed_at")] + public readonly DateTime? RenewedAt; /// /// The time when the access level will expire (could be in the past and could be null for lifetime access). /// - public readonly DateTime? ExpiresAt; // nullable + [DataMember(Name = "expires_at")] + public readonly DateTime? ExpiresAt; /// /// Whether the access level is active for a lifetime (no expiration date). /// /// - /// If set to true you shouldn't check expires_at , or you could just check isActive. + /// If set to true you shouldn't check expires_at , or you could just check isActive. /// + [DataMember(Name = "is_lifetime", IsRequired = true)] public readonly bool IsLifetime; /// /// The type of active introductory offer. /// /// - /// Possible values are: free_trial, pay_as_you_go, pay_up_front. - /// If the value is not null, it means that the offer was applied during the current subscription period. + /// Possible values are: free_trial, pay_as_you_go, pay_up_front. + /// If the value is not null, it means that the offer was applied during the current subscription period. /// - public readonly string ActiveIntroductoryOfferType; // nullable + [DataMember(Name = "active_introductory_offer_type")] + public readonly string ActiveIntroductoryOfferType; /// /// The type of active promotional offer. @@ -78,23 +87,34 @@ public partial class AccessLevel /// Possible values are: free_trial, pay_as_you_go, pay_up_front. /// If the value is not null, it means that the offer was applied during the current subscription period. /// - public readonly string ActivePromotionalOfferType; // nullable + [DataMember(Name = "active_promotional_offer_type")] + public readonly string ActivePromotionalOfferType; /// - /// An identifier of active promotional offer. + /// The App Store promotional offer that unlocked this access level. Null when the + /// purchase used none, and for a purchase made on Android — the profile is one object + /// across platforms, so this says where the purchase happened, not where the app runs. /// - public readonly string ActivePromotionalOfferId; // nullable + [DataMember(Name = "active_promotional_offer_id")] + public readonly string ActivePromotionalOfferId; - public readonly string OfferId; // nullable + /// + /// The Google Play offer that unlocked this access level. Null when the purchase used + /// none, and for a purchase made on iOS — see . + /// + [DataMember(Name = "offer_id")] + public readonly string OfferId; /// /// Whether the auto-renewable subscription is set to renew. /// + [DataMember(Name = "will_renew", IsRequired = true)] public readonly bool WillRenew; /// /// Whether the auto-renewable subscription is in the grace period. /// + [DataMember(Name = "is_in_grace_period", IsRequired = true)] public readonly bool IsInGracePeriod; /// @@ -102,37 +122,50 @@ public partial class AccessLevel /// /// /// Subscription can still be active, it just means that auto-renewal turned off. - /// Will be set to null if the user reactivates the subscription. + /// Will be set to null if the user reactivates the subscription. /// - public readonly DateTime? UnsubscribedAt; // nullable + [DataMember(Name = "unsubscribed_at")] + public readonly DateTime? UnsubscribedAt; /// - /// The time when billing issue was detected (Apple was not able to charge the card). + /// The time a billing issue was detected — the store could not charge the payment method. /// /// - /// Subscription can still be active. Will be set to null if the charge will be made. + /// Subscription can still be active. Will be set to null if the charge will be made. /// - public readonly DateTime? BillingIssueDetectedAt; // nullable + [DataMember(Name = "billing_issue_detected_at")] + public readonly DateTime? BillingIssueDetectedAt; /// - /// The time when the access level has started (could be in the future). + /// The time when the access level has started (could be in the future). Null when there is + /// no start date. /// - public readonly DateTime? StartsAt; // nullable + [DataMember(Name = "starts_at")] + public readonly DateTime? StartsAt; /// - /// The reason why the subscription was cancelled. + /// The reason why the subscription was cancelled. Null when it was not. /// /// - /// Possible values are: voluntarily_cancelled, billing_error, refund, price_increase, product_was_not_available, unknown. + /// The values the native SDKs list: voluntarily_cancelled, billing_error, + /// price_increase, product_was_not_available, refund, upgraded, unknown. It stays a + /// string rather than an enum because the contract leaves the set open — do not write + /// a switch that assumes these are all of them. /// - public readonly string CancellationReason; // nullable + [DataMember(Name = "cancellation_reason")] + public readonly string CancellationReason; /// /// Whether the purchase was refunded. /// + [DataMember(Name = "is_refund", IsRequired = true)] public readonly bool IsRefund; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Id)}: {Id}, " + $"{nameof(IsActive)}: {IsActive}, " + $"{nameof(VendorProductId)}: {VendorProductId}, " + @@ -154,4 +187,4 @@ public override string ToString() => $"{nameof(Id)}: {Id}, " + $"{nameof(IsRefund)}: {IsRefund}"; } } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.NonSubscription.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.NonSubscription.cs index 368fede..8f131ed 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.NonSubscription.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.NonSubscription.cs @@ -1,24 +1,27 @@ -// -// AdaptyProfile.NonSubscription.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - using System; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyProfile + [Preserve] + public sealed partial class AdaptyProfile { - public partial class NonSubscription + /// + /// One non-subscription purchase of a profile — consumable or lifetime. + /// + [DataContract] + public sealed class NonSubscription { + private NonSubscription() { } + /// /// The identifier of the purchase in Adapty. /// /// /// You can use it to ensure that you've already processed this purchase (for example tracking one time products). /// + [DataMember(Name = "purchase_id", IsRequired = true)] public readonly string PurchaseId; /// @@ -27,46 +30,52 @@ public partial class NonSubscription /// /// The possible values are: app_store, play_store, adapty. /// + [DataMember(Name = "store", IsRequired = true)] public readonly string Store; /// - /// The identifier of the product in the App Store Connect. + /// The identifier of the product in the store it was bought from. /// + [DataMember(Name = "vendor_product_id", IsRequired = true)] public readonly string VendorProductId; /// - /// Transaction id from the App Store. + /// The transaction id the store reported. Null when it does not report one. /// - public readonly string VendorTransactionId; // nullable + [DataMember(Name = "vendor_transaction_id")] + public readonly string VendorTransactionId; /// /// The time when the product was purchased. /// + [DataMember(Name = "purchased_at", IsRequired = true)] public readonly DateTime PurchasedAt; /// /// Whether the product was purchased in the sandbox environment. /// + [DataMember(Name = "is_sandbox", IsRequired = true)] public readonly bool IsSandbox; /// /// Whether the purchase was refunded. /// + [DataMember(Name = "is_refund", IsRequired = true)] public readonly bool IsRefund; - /// - /// Deprecated, use 'IsConsumable'. - /// - public bool IsOneTime => IsConsumable; - /// /// Whether the product should only be processed once. /// /// /// If true, the purchase will be returned by Adapty API one time only. /// + [DataMember(Name = "is_consumable", IsRequired = true)] public readonly bool IsConsumable; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(PurchaseId)}: {PurchaseId}, " + $"{nameof(VendorProductId)}: {VendorProductId}, " + $"{nameof(Store)}: {Store}, " + @@ -77,4 +86,4 @@ public override string ToString() => $"{nameof(PurchaseId)}: {PurchaseId}, " + $"{nameof(IsRefund)}: {IsRefund}"; } } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.Subscription.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.Subscription.cs index bf70d3d..fa8dbc0 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.Subscription.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.Subscription.cs @@ -1,103 +1,180 @@ -// -// AdaptyProfile.Subscription.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - using System; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyProfile + public sealed partial class AdaptyProfile { - public partial class Subscription + /// + /// One subscription of a profile, as the store and Adapty currently see it. + /// + [DataContract] + public sealed class Subscription { + private Subscription() { } + + /// /// The store of the purchase. The possible values are: app_store, play_store , adapty. + /// + [DataMember(Name = "store", IsRequired = true)] public readonly string Store; - /// The identifier of the product in the App Store Connect. + /// + /// The identifier of the product in the store it was bought from. + /// + [DataMember(Name = "vendor_product_id", IsRequired = true)] public readonly string VendorProductId; - /// Transaction id from the App Store. + /// + /// The transaction id the store reported. + /// + [DataMember(Name = "vendor_transaction_id", IsRequired = true)] public readonly string VendorTransactionId; - /// Original transaction id from the App Store. + /// + /// The original transaction id the store reported. + /// /** * For auto-renewable subscription, this will be the id of the first transaction in the subscription. */ + [DataMember(Name = "vendor_original_transaction_id", IsRequired = true)] public readonly string VendorOriginalTransactionId; + /// /// Whether the subscription is active. + /// + [DataMember(Name = "is_active", IsRequired = true)] public readonly bool IsActive; + /// /// Whether the subscription is active for a lifetime (no expiration date). + /// /** * If set to true you shouldn't check expires_at , or you could just check isActive. */ + [DataMember(Name = "is_lifetime", IsRequired = true)] public readonly bool IsLifetime; + /// /// The time when the subscription was activated. + /// + [DataMember(Name = "activated_at", IsRequired = true)] public readonly DateTime ActivatedAt; - /// The time when the subscription was renewed. - public readonly DateTime? RenewedAt; // nullable + /// + /// The time when the subscription was renewed. Null when there has been no renewal. + /// + [DataMember(Name = "renewed_at")] + public readonly DateTime? RenewedAt; + /// /// The time when the subscription will expire (could be in the past and could be null for lifetime access). - public readonly DateTime? ExpiresAt; // nullable - - /// The time when the subscription has started (could be in the future). - public readonly DateTime? StartsAt; // nullable - + /// + [DataMember(Name = "expires_at")] + public readonly DateTime? ExpiresAt; + + /// + /// The time when the subscription has started (could be in the future). Null when there is + /// no start date. + /// + [DataMember(Name = "starts_at")] + public readonly DateTime? StartsAt; + + /// /// The time when the auto-renewable subscription was cancelled. + /// /** * Subscription can still be active, it just means that auto-renewal turned off. * Will be set to null if the user reactivates the subscription. */ - public readonly DateTime? UnsubscribedAt; // nullable + [DataMember(Name = "unsubscribed_at")] + public readonly DateTime? UnsubscribedAt; - /// The time when billing issue was detected (Apple was not able to charge the card). + /// + /// The time a billing issue was detected — the store could not charge the payment method. + /// /** * Subscription can still be active. Will be set to null if the charge will be made. */ - public readonly DateTime? BillingIssueDetectedAt; // nullable + [DataMember(Name = "billing_issue_detected_at")] + public readonly DateTime? BillingIssueDetectedAt; + /// /// Whether the auto-renewable subscription is in the grace period. + /// + [DataMember(Name = "is_in_grace_period", IsRequired = true)] public readonly bool IsInGracePeriod; + /// /// Whether the product was purchased in the sandbox environment. + /// + [DataMember(Name = "is_sandbox", IsRequired = true)] public readonly bool IsSandbox; + /// /// Whether the purchase was refunded. + /// + [DataMember(Name = "is_refund", IsRequired = true)] public readonly bool IsRefund; + /// /// Whether the auto-renewable subscription is set to renew. + /// + [DataMember(Name = "will_renew", IsRequired = true)] public readonly bool WillRenew; + /// /// The type of active introductory offer. + /// /** * Possible values are: free_trial, pay_as_you_go, pay_up_front. * If the value is not null, it means that the offer was applied during the current subscription period. */ - public readonly string ActiveIntroductoryOfferType; // nullable + [DataMember(Name = "active_introductory_offer_type")] + public readonly string ActiveIntroductoryOfferType; + /// /// The type of active promotional offer. + /// /** * Possible values are: free_trial, pay_as_you_go, pay_up_front. * If the value is not null, it means that the offer was applied during the current subscription period. */ - public readonly string ActivePromotionalOfferType; // nullable - - public readonly string ActivePromotionalOfferId; // nullable - - public readonly string OfferId; // nullable - - /// The reason why the subscription was cancelled. - /** - * Possible values are: voluntarily_cancelled, billing_error, refund, price_increase, product_was_not_available, unknown. - */ - public readonly string CancellationReason; // nullable - + [DataMember(Name = "active_promotional_offer_type")] + public readonly string ActivePromotionalOfferType; + + /// + /// The App Store promotional offer in force right now. Null when there is none, and + /// for a subscription bought on Android — the profile is one object across platforms, + /// so this says where the purchase happened, not where the app runs. + /// + [DataMember(Name = "active_promotional_offer_id")] + public readonly string ActivePromotionalOfferId; + + /// + /// The Google Play offer the current period was bought with. Null when there was none, + /// and for a subscription bought on iOS — see . + /// + [DataMember(Name = "offer_id")] + public readonly string OfferId; + + /// + /// The reason why the subscription was cancelled. Null when it was not. + /// + /// + /// The values the native SDKs list: voluntarily_cancelled, billing_error, + /// price_increase, product_was_not_available, refund, upgraded, unknown. It stays a + /// string rather than an enum because the contract leaves the set open — do not write + /// a switch that assumes these are all of them. + /// + [DataMember(Name = "cancellation_reason")] + public readonly string CancellationReason; + + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(IsActive)}: {IsActive}, " + $"{nameof(VendorProductId)}: {VendorProductId}, " + $"{nameof(Store)}: {Store}, " + @@ -120,4 +197,4 @@ public override string ToString() => $"{nameof(IsActive)}: {IsActive}, " + $"{nameof(IsRefund)}: {IsRefund}"; } } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.cs index cad432f..e451045 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfile.cs @@ -1,13 +1,9 @@ -// -// AdaptyProfile.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { @@ -18,11 +14,15 @@ namespace AdaptySDK /// The profile contains all information about the user including access levels, subscriptions, non-subscription purchases, and custom attributes. /// Read more at Adapty Documentation /// - public partial class AdaptyProfile + [DataContract] + public sealed partial class AdaptyProfile { + private AdaptyProfile() => Freeze(); + /// /// An identifier of the user in Adapty. /// + [DataMember(Name = "profile_id", IsRequired = true)] public readonly string ProfileId; /// @@ -31,22 +31,39 @@ public partial class AdaptyProfile /// /// This is the customer user ID that you set using . /// + [DataMember(Name = "customer_user_id")] public readonly string CustomerUserId; /// /// An identifier of the segment to which the user belongs. /// + [DataMember(Name = "segment_hash", IsRequired = true)] internal readonly string SegmentId; /// /// Identifiers of attribution sources applied to the profile. /// - public readonly IList AppliedAttributionSources; + [DataMember(Name = "applied_attribution_sources")] + private readonly List _AppliedAttributionSources = new List(); + + /// + /// The attribution sources applied to this profile. + /// + [Preserve] + public IReadOnlyList AppliedAttributionSources { get; private set; } /// /// Previously set user custom attributes with method. /// - public readonly IDictionary CustomAttributes; + [DataMember(Name = "custom_attributes")] + [Newtonsoft.Json.JsonConverter(typeof(Serialization.AdaptyConverterLooseJson))] + private readonly Dictionary _CustomAttributes = new Dictionary(); + + /// + /// The custom attributes set on this profile. Numbers arrive as . + /// + [Preserve] + public IReadOnlyDictionary CustomAttributes { get; private set; } /// /// A dictionary of access levels configured in the Adapty Dashboard. @@ -56,7 +73,15 @@ public partial class AdaptyProfile /// The values are objects. /// Can be null if the customer has no access levels. /// - public readonly IDictionary AccessLevels; + [DataMember(Name = "paid_access_levels")] + private readonly Dictionary _AccessLevels = new Dictionary(); + + /// + /// The profile's access levels, keyed by the identifier configured in the Dashboard. Empty when + /// the user has none. + /// + [Preserve] + public IReadOnlyDictionary AccessLevels { get; private set; } /// /// A dictionary of active subscriptions. @@ -66,7 +91,14 @@ public partial class AdaptyProfile /// The values are objects. /// Can be null if the customer has no subscriptions. /// - public readonly IDictionary Subscriptions; + [DataMember(Name = "subscriptions")] + private readonly Dictionary _Subscriptions = new Dictionary(); + + /// + /// The profile's subscriptions, keyed by store product id. Empty when the user has none. + /// + [Preserve] + public IReadOnlyDictionary Subscriptions { get; private set; } /// /// A dictionary of non-subscription purchases. @@ -76,12 +108,49 @@ public partial class AdaptyProfile /// The values are lists of objects (one product can have multiple purchases). /// Can be null if the customer has no non-subscription purchases. /// - public readonly IDictionary> NonSubscriptions; + [DataMember(Name = "non_subscriptions")] + private readonly Dictionary> _NonSubscriptions = new Dictionary>(); + + /// + /// The profile's non-subscription purchases, keyed by store product id — a list each, since one + /// product can be bought more than once. Empty when the user has none. + /// + [Preserve] + public IReadOnlyDictionary> NonSubscriptions { get; private set; } + [DataMember(Name = "timestamp", IsRequired = true)] internal readonly Int64 Version; + [DataMember(Name = "is_test_user", IsRequired = true)] internal readonly bool IsTestUser; + // Replace hands the deserializer a new collection instead of filling the one the field + // initializer made, so the views are built here rather than alongside it. + [Preserve] + [OnDeserialized] + private void OnDeserialized(StreamingContext context) => Freeze(); + + private void Freeze() + { + AppliedAttributionSources = new ReadOnlyCollection(_AppliedAttributionSources); + CustomAttributes = new ReadOnlyDictionary(_CustomAttributes); + AccessLevels = new ReadOnlyDictionary(_AccessLevels); + Subscriptions = new ReadOnlyDictionary(_Subscriptions); + + var nonSubscriptions = new Dictionary>(); + foreach (var entry in _NonSubscriptions) + { + nonSubscriptions[entry.Key] = new ReadOnlyCollection(entry.Value); + } + NonSubscriptions = new ReadOnlyDictionary>( + nonSubscriptions + ); + } + + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() { var customAttributesStr = diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileGender.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileGender.cs index fa527b5..4948af1 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileGender.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileGender.cs @@ -1,16 +1,28 @@ -// -// AdaptyProfileGender.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// + /// The gender recorded on a profile. + /// + [Preserve] public enum AdaptyProfileGender { - Female, - Male, - Other, + /// + /// Female. + /// + [EnumMember(Value = "f")] + Female = 0, + /// + /// Male. + /// + [EnumMember(Value = "m")] + Male = 1, + /// + /// Anything else, including a user who prefers not to say. + /// + [EnumMember(Value = "o")] + Other = 2, } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileParameters.Builder.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileParameters.Builder.cs index 42b5451..d201818 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileParameters.Builder.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileParameters.Builder.cs @@ -1,51 +1,75 @@ -// -// AdaptyProfileParameters.Builder.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - using System; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyProfileParameters + [Preserve] + public sealed partial class AdaptyProfileParameters { - public class Builder + /// + /// Assembles an . Every setter returns the builder, + /// so calls chain; what is never set is not sent, and is therefore left as it is on the + /// server rather than cleared. + /// + public sealed class Builder { private AdaptyProfileParameters _Parameters = new AdaptyProfileParameters(); + /// + /// Sets . + /// + /// The user's first name. public Builder SetFirstName(string value) { _Parameters.FirstName = value; return this; } + /// + /// Sets . + /// + /// The user's last name. public Builder SetLastName(string value) { _Parameters.LastName = value; return this; } + /// + /// Sets . + /// + /// The user's gender. public Builder SetGender(AdaptyProfileGender? value) { _Parameters.Gender = value; return this; } + /// + /// Sets . Sent as a calendar date, so the time of day is ignored. + /// + /// The user's date of birth. public Builder SetBirthday(DateTime? value) { _Parameters.Birthday = value; return this; } + /// + /// Sets . + /// + /// The user's email address. public Builder SetEmail(string value) { _Parameters.Email = value; return this; } + /// + /// Sets . + /// + /// The user's phone number. public Builder SetPhoneNumber(string value) { _Parameters.PhoneNumber = value; @@ -53,37 +77,67 @@ public Builder SetPhoneNumber(string value) } + /// + /// Sets . iOS only. + /// + /// What the user answered to the tracking prompt. public Builder SetAppTrackingTransparencyStatus(AppTrackingTransparencyStatus? value) { _Parameters.AppTrackingTransparencyStatus = value; return this; } + /// + /// Sets . + /// + /// True to switch analytics off for this profile. public Builder SetAnalyticsDisabled(bool? value) { _Parameters.AnalyticsDisabled = value; return this; } + /// + /// Sets a custom attribute to a string value. Same limits as + /// , and the same + /// exception when they are broken. + /// + /// Up to 30 characters of letters, digits, dashes, points and underscores. + /// Between 1 and 50 characters. public Builder SetCustomStringAttribute(string key, string value) { _Parameters.SetCustomStringAttribute(key, value); return this; } + /// + /// Sets a custom attribute to a numeric value. Same limits as + /// . + /// + /// Up to 30 characters of letters, digits, dashes, points and underscores. + /// The value to store. public Builder SetCustomDoubleAttribute(string key, double value) { _Parameters.SetCustomDoubleAttribute(key, value); return this; } + /// + /// Clears a custom attribute, the way + /// does — sent as an + /// explicit removal rather than left out. + /// + /// The key to clear. public Builder RemoveCustomAttribute(string key) { _Parameters.RemoveCustomAttribute(key); return this; } + /// + /// The parameters described by this builder. + /// public AdaptyProfileParameters Build() => _Parameters; } } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileParameters.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileParameters.cs index d731b54..a90a14b 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileParameters.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyProfileParameters.cs @@ -1,33 +1,108 @@ -// -// AdaptyProfileParameters.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Runtime.Serialization; using System.Text.RegularExpressions; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyProfileParameters + /// + /// The profile attributes sends. Build one with + /// . + /// + /// + /// Only what is set is sent — a field left null is not cleared on the server, it is left + /// alone. To clear a custom attribute use , which sends an + /// explicit removal. + /// + [DataContract] + public sealed partial class AdaptyProfileParameters { + /// + /// The user's first name. Null leaves whatever the profile already has. + /// + [DataMember(Name = "first_name")] public string FirstName; + /// + /// The user's last name. Null leaves whatever the profile already has. + /// + [DataMember(Name = "last_name")] public string LastName; + /// + /// The user's gender. Null leaves whatever the profile already has. + /// + [DataMember(Name = "gender")] public AdaptyProfileGender? Gender; + /// + /// The user's date of birth. Sent as a calendar date — yyyy-MM-dd — so the time of + /// day and the are ignored, unlike the dates the SDK hands back. + /// public DateTime? Birthday; + /// + /// The user's email address. Null leaves whatever the profile already has. + /// + [DataMember(Name = "email")] public string Email; + /// + /// The user's phone number. Null leaves whatever the profile already has. + /// + [DataMember(Name = "phone_number")] public string PhoneNumber; + /// + /// iOS only. What the user answered to the App Tracking Transparency prompt. Sent on iOS + /// alone — the contract has no such key for Android. + /// +#if UNITY_IOS + [DataMember(Name = "att_status")] +#endif public AppTrackingTransparencyStatus? AppTrackingTransparencyStatus; + /// + /// Switches analytics off for this profile. Calls that need analytics then fail with + /// . + /// + [DataMember(Name = "analytics_disabled")] public bool? AnalyticsDisabled; private Dictionary _CustomAttributes = new Dictionary(); - public Dictionary CustomAttributes => _CustomAttributes; - + /// + /// The custom attributes set so far, as a read-only view. A key removed with + /// is present here with a null value, which is what + /// tells the server to clear it. + /// + [Preserve] + public IReadOnlyDictionary CustomAttributes => + new ReadOnlyDictionary(_CustomAttributes); + + + /// + /// The contract wants a plain calendar date here, not the timestamp format of the other + /// dates, so this one is written by hand rather than through the date converter. + /// + [DataMember(Name = "birthday")] + [Preserve] + private string BirthdayForRequest => + Birthday?.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture); + + [DataMember(Name = "custom_attributes")] + [Preserve] + private System.Collections.Generic.Dictionary CustomAttributesForRequest => + _CustomAttributes.Count > 0 ? _CustomAttributes : null; + + /// + /// Sets a custom attribute to a string value. + /// + /// + /// Up to 30 characters of letters, digits, dashes, points and underscores. + /// + /// Between 1 and 50 characters. + /// + /// The key or the value breaks those limits, or the profile would end up with more than 30 + /// custom attributes. + /// public void SetCustomStringAttribute(string key, string value) { if (string.IsNullOrEmpty(value) || value.Length > 50) @@ -42,6 +117,17 @@ public void SetCustomStringAttribute(string key, string value) } + /// + /// Sets a custom attribute to a numeric value. + /// + /// + /// Up to 30 characters of letters, digits, dashes, points and underscores. + /// + /// The value to store. + /// + /// The key breaks those limits, or the profile would end up with more than 30 custom + /// attributes. + /// public void SetCustomDoubleAttribute(string key, double value) { if (!_validateCustomAttributeKey(key, true)) @@ -51,6 +137,12 @@ public void SetCustomDoubleAttribute(string key, double value) _CustomAttributes[key] = value; } + /// + /// Clears a custom attribute. The key is sent with a null value rather than left out, so + /// the server removes it instead of leaving it as it was. + /// + /// The key to clear. + /// The key is not a valid custom attribute key. public void RemoveCustomAttribute(string key) { if (!_validateCustomAttributeKey(key, false)) @@ -88,6 +180,10 @@ bool _validateCustomAttributeKey(String addingKey, bool testCount) return true; } + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(FirstName)}: {FirstName}, " + $"{nameof(LastName)}: {LastName}, " + @@ -100,4 +196,4 @@ public override string ToString() => $"{nameof(CustomAttributes)}: {CustomAttributes}"; } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseParameters.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseParameters.cs index 0664056..8975eb4 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseParameters.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseParameters.cs @@ -1,17 +1,40 @@ -// -// AdaptyPurchaseParameters.cs -// AdaptySDK -// -// Created by Alexey Goncharov on 10.09.2025. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyPurchaseParameters + /// + /// The optional extras of a purchase. Both are Android only — on iOS an instance changes + /// nothing, and + /// without one is the same call. + /// + [DataContract] + [Preserve] + public sealed class AdaptyPurchaseParameters { - public readonly AdaptySubscriptionUpdateParameters SubscriptionUpdateParams; // Android Only, nullable - public readonly bool? IsOfferPersonalized; // Android Only, nullable + /// + /// Android only. Makes the purchase replace a subscription the user already has. Null for + /// an ordinary purchase. + /// + [DataMember(Name = "subscription_update_params")] + public readonly AdaptySubscriptionUpdateParameters SubscriptionUpdateParams; + /// + /// Android only. Declares to Google Play that the price shown was personalised to this + /// user, which some jurisdictions require disclosing. Null leaves the native default. + /// + [DataMember(Name = "is_offer_personalized")] + public readonly bool? IsOfferPersonalized; + + /// + /// Android only. The subscription this purchase replaces, or null. + /// + /// + /// Android only. Whether the price shown was personalised, or null. + /// + /// + /// Builds the extras for one purchase. Both are Android only. + /// public AdaptyPurchaseParameters( AdaptySubscriptionUpdateParameters subscriptionUpdateParams = null, bool? isOfferPersonalized = null @@ -21,15 +44,28 @@ public AdaptyPurchaseParameters( IsOfferPersonalized = isOfferPersonalized; } + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(SubscriptionUpdateParams)}: {SubscriptionUpdateParams}, " + $"{nameof(IsOfferPersonalized)}: {IsOfferPersonalized}"; } - public class AdaptyPurchaseParametersBuilder + /// + /// Assembles an . Every setter returns the builder, so + /// calls chain; the constructor takes both values directly if that reads better. + /// + [Preserve] + public sealed class AdaptyPurchaseParametersBuilder { private AdaptyPurchaseParameters _parameters = new AdaptyPurchaseParameters(); + /// + /// Sets . Android only. + /// + /// The subscription this purchase replaces. public AdaptyPurchaseParametersBuilder SetSubscriptionUpdateParams( AdaptySubscriptionUpdateParameters subscriptionUpdateParams ) @@ -41,6 +77,10 @@ AdaptySubscriptionUpdateParameters subscriptionUpdateParams return this; } + /// + /// Sets . Android only. + /// + /// Whether the price shown was personalised to this user. public AdaptyPurchaseParametersBuilder SetIsOfferPersonalized(bool? isOfferPersonalized) { _parameters = new AdaptyPurchaseParameters( @@ -50,6 +90,9 @@ public AdaptyPurchaseParametersBuilder SetIsOfferPersonalized(bool? isOfferPerso return this; } + /// + /// The parameters described by this builder. + /// public AdaptyPurchaseParameters Build() { return _parameters; diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseResult.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseResult.cs index db71700..829b0fe 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseResult.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseResult.cs @@ -1,24 +1,50 @@ -// -// AdaptyPurchaseResult.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 17.12.2024. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyPurchaseResult + /// + /// How a purchase ended, and the updated profile when it succeeded. + /// + [DataContract] + [Preserve] + public sealed class AdaptyPurchaseResult { + private AdaptyPurchaseResult() { } + + /// + /// How the purchase ended. + /// + [DataMember(Name = "type", IsRequired = true)] public readonly AdaptyPurchaseResultType Type; + /// + /// The profile as it stands after the purchase. Only for + /// — null otherwise. + /// + [DataMember(Name = "profile")] public readonly AdaptyProfile Profile; - public readonly string AppleJWSTransaction; // nullable, iOS Only + /// + /// iOS only. The signed App Store transaction, for server-side verification of your own. + /// Null off iOS, and when the store does not provide one. + /// + [DataMember(Name = "apple_jws_transaction")] + public readonly string AppleJWSTransaction; - public readonly string GooglePurchaseToken; // nullable, Android Only + /// + /// Android only. The Google Play purchase token, for server-side verification of your own. + /// Null off Android, and when the store does not provide one. + /// + [DataMember(Name = "google_purchase_token")] + public readonly string GooglePurchaseToken; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Type)}: {Type}, " - + $"{nameof(Profile)}: {Profile.ToString()}, " + + $"{nameof(Profile)}: {(Profile == null ? "null" : Profile.ToString())}, " + $"{nameof(AppleJWSTransaction)}: {(string.IsNullOrEmpty(AppleJWSTransaction) ? "null or empty" : AppleJWSTransaction)}, " + $"{nameof(GooglePurchaseToken)}: {(string.IsNullOrEmpty(GooglePurchaseToken) ? "null or empty" : GooglePurchaseToken)}"; } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseResultType.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseResultType.cs index ad582f3..23fcb51 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseResultType.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyPurchaseResultType.cs @@ -1,16 +1,28 @@ -// -// AdaptyPurchaseResultType.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 17.12.2024. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// + /// How a purchase ended. + /// + [Preserve] public enum AdaptyPurchaseResultType { - Pending, - UserCancelled, - Success + /// + /// The store is waiting on something — Ask to Buy, or a payment method that settles later. The profile updates when it resolves, so wait rather than retrying. + /// + [EnumMember(Value = "pending")] + Pending = 0, + /// + /// The user dismissed the store's sheet. Not a failure to report. + /// + [EnumMember(Value = "user_cancelled")] + UserCancelled = 1, + /// + /// The purchase went through; the updated profile is on the result. + /// + [EnumMember(Value = "success")] + Success = 2, } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyRefundPreference.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyRefundPreference.cs index 85b9f3d..95783a2 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyRefundPreference.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyRefundPreference.cs @@ -1,30 +1,31 @@ -// -// AdaptyRefundPreference.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 19.03.2025. -// - -using System; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// + /// iOS only. What to tell the App Store when it consults you about a refund request for this user. + /// + /// + /// A preference, not a decision — the App Store is not obliged to follow it. + /// + [Preserve] public enum AdaptyRefundPreference { - NoPreference, - Grant, - Decline, - } - - public static partial class AdaptyRefundPreferenceExtensions - { - public static string ToJSONNode(this AdaptyRefundPreference value) => - value switch - { - AdaptyRefundPreference.NoPreference => "no_preference", - AdaptyRefundPreference.Grant => "grant", - AdaptyRefundPreference.Decline => "decline", - _ => throw new Exception($"AdaptyRefundPreference unknown value: {value}"), - }; + /// + /// Express no preference and let the App Store decide. + /// + [EnumMember(Value = "no_preference")] + NoPreference = 0, + /// + /// Ask the App Store to grant the refund. + /// + [EnumMember(Value = "grant")] + Grant = 1, + /// + /// Ask the App Store to decline it. + /// + [EnumMember(Value = "decline")] + Decline = 2, } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyRemoteConfig.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyRemoteConfig.cs index 23a8263..1371af7 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyRemoteConfig.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyRemoteConfig.cs @@ -1,21 +1,35 @@ -// AdaptyRemoteConfig.cs -// AdaptySDK -// -// Created by Aleksei Goncharov on 09.09.2025. - using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Runtime.Serialization; +using AdaptySDK.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - using AdaptySDK.SimpleJSON; - - public partial class AdaptyRemoteConfig + /// + /// The JSON configured against a flow in the Dashboard, for one localization. + /// + [DataContract] + [Preserve] + public sealed class AdaptyRemoteConfig { + private AdaptyRemoteConfig() { } + + /// + /// The localization this config belongs to. + /// + [DataMember(Name = "lang", IsRequired = true)] public readonly string Locale; + /// + /// The configured JSON, as the string it was written as. parses it. + /// + [DataMember(Name = "data", IsRequired = true)] public readonly string Data; + /// /// A custom dictionary configured in Adapty Dashboard for this paywall (same as `remoteConfigString`) - public IDictionary Dictionary + /// + public IReadOnlyDictionary Dictionary { get { @@ -24,7 +38,9 @@ public IDictionary Dictionary return null; } - return JSONNode.Parse(Data).GetDictionary(); + return new ReadOnlyDictionary( + AdaptyJson.DeserializeRemoteConfigDictionary(Data) + ); } } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyResult.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyResult.cs index c330e8d..e928d1a 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyResult.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyResult.cs @@ -1,18 +1,18 @@ -// -// AdaptyResult.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 14.01.2022. -// +using UnityEngine.Scripting; namespace AdaptySDK { + [Preserve] internal class AdaptyResult { public readonly AdaptyError Error; public readonly T Value; - public override string ToString() => + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// + public override string ToString() => $"{nameof(Value)}: {Value}, " + $"{nameof(Error)}: {Error}"; @@ -22,4 +22,4 @@ internal AdaptyResult(T value, AdaptyError error) Value = value; } } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyServerCluster.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyServerCluster.cs index 1995c8d..5df11ed 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyServerCluster.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyServerCluster.cs @@ -1,16 +1,28 @@ -// -// AdaptyServerCluster.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 10.12.2024. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// + /// Which Adapty server region the SDK talks to. Set it in the configuration to match where your account's data is held. + /// + [Preserve] public enum AdaptyServerCluster { - Default, - EU, - CN, + /// + /// The default cluster. + /// + [EnumMember(Value = "default")] + Default = 0, + /// + /// The European Union cluster. + /// + [EnumMember(Value = "eu")] + EU = 1, + /// + /// The mainland China cluster. + /// + [EnumMember(Value = "cn")] + CN = 2, } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscription.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscription.cs index edfbcfa..6805407 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscription.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscription.cs @@ -1,35 +1,70 @@ -// -// AdaptySubscription.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptySubscription + /// + /// What a product's subscription looks like: its period, its offer, and how it renews. + /// + [DataContract] + [Preserve] + public sealed class AdaptySubscription { + private AdaptySubscription() { } + + /// /// The identifier of the subscription group to which the subscription belongs. /// /// [Nullable] + /// +#if UNITY_IOS + [DataMember(Name = "group_identifier", IsRequired = true)] +#endif public readonly string GroupIdentifier; + /// /// A ProductSubscriptionPeriodModel object. /// The period details for products that are subscriptions. /// + /// + [DataMember(Name = "period", IsRequired = true)] public readonly AdaptySubscriptionPeriod Period; + /// /// Localized subscription period of the product. /// /// [Nullable] + /// + [DataMember(Name = "localized_period")] public readonly string LocalizedPeriod; + /// + /// The discounted offer attached to this subscription, or null when it is at full price. + /// + [DataMember(Name = "offer")] public readonly AdaptySubscriptionOffer Offer; - public readonly AdaptySubscriptionRenewalType RenewalType; - public readonly string BasePlanId; //nullable + /// + /// Android only. Whether the subscription renews by itself. + /// +#if UNITY_ANDROID + [DataMember(Name = "renewal_type", IsRequired = true)] +#endif + public readonly AdaptySubscriptionRenewalType RenewalType = + AdaptySubscriptionRenewalType.Autorenewable; + /// + /// Android only. The Google Play base plan this subscription is on. Null on iOS. + /// +#if UNITY_ANDROID + [DataMember(Name = "base_plan_id", IsRequired = true)] +#endif + public readonly string BasePlanId; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(GroupIdentifier)}: {GroupIdentifier}, " + $"{nameof(Period)}: {Period}, " + @@ -38,4 +73,4 @@ public override string ToString() => $"{nameof(RenewalType)}: {RenewalType}, " + $"{nameof(BasePlanId)}: {BasePlanId}"; } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionOffer.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionOffer.cs index 18d0f8d..8fac8f7 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionOffer.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionOffer.cs @@ -1,27 +1,64 @@ -// -// AdaptySubscriptionOffer.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 12.12.2024. -// - using System.Collections.Generic; +using System.Collections.ObjectModel; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptySubscriptionOffer + /// + /// The contract nests the identifier and the type inside offer_identifier while the + /// model keeps them flat, so this one is built by + /// AdaptySDK.Serialization.AdaptyConverterSubscriptionOffer rather than from member + /// annotations. + /// + /// + /// A discounted offer on a subscription, and the phases it runs through. + /// + [Preserve] + public sealed class AdaptySubscriptionOffer { + internal AdaptySubscriptionOffer( + string identifier, + AdaptySubscriptionOfferType type, + IList phases, + IList offerTags + ) + { + Identifier = identifier; + Type = type; + Phases = new ReadOnlyCollection(phases); + + // No platform check: the converter is the only caller and already reads offer_tags on + // Android alone, so off it this is null on the way in. + OfferTags = offerTags is null ? null : new ReadOnlyCollection(offerTags); + } + + /// + /// The offer id the store knows it by. Null for an introductory offer on iOS, which has none. + /// public readonly string Identifier; + /// + /// Which kind of offer this is. + /// public readonly AdaptySubscriptionOfferType Type; - public readonly IList Phases; - public readonly IList OfferTags; + /// + /// The phases the offer runs through, in order. + /// + public readonly IReadOnlyList Phases; + /// + /// Android only. The tags Google Play carries on the offer. Null on iOS. + /// + public readonly IReadOnlyList OfferTags; - public override string ToString() => + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// + public override string ToString() => $"{nameof(Identifier)}: {Identifier}, " + $"{nameof(Type)}: {Type}, " + $"{nameof(Phases)}: {Phases}, " + $"{nameof(OfferTags)}: {OfferTags}"; } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionOfferType.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionOfferType.cs index 6d419bf..69fe376 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionOfferType.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionOfferType.cs @@ -1,17 +1,33 @@ -// -// AdaptySubscriptionOfferType.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// + /// Which kind of discounted offer a subscription phase belongs to. + /// + [Preserve] public enum AdaptySubscriptionOfferType { - Introductory, - Promotional, - WinBack, - Code // iOS Only + /// + /// The offer for a user who has never subscribed to this product. + /// + [EnumMember(Value = "introductory")] + Introductory = 0, + /// + /// An offer aimed at an existing or lapsed subscriber, identified by an offer id. + /// + [EnumMember(Value = "promotional")] + Promotional = 1, + /// + /// An offer aimed at a user whose subscription has ended. + /// + [EnumMember(Value = "win_back")] + WinBack = 2, + /// + /// iOS only. An offer redeemed through an App Store offer code. + /// + [EnumMember(Value = "code")] + Code = 3, } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPeriod.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPeriod.cs index 74e70ab..0f1c057 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPeriod.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPeriod.cs @@ -1,18 +1,33 @@ -// -// AdaptySubscriptionPeriod.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptySubscriptionPeriod + /// + /// A span of time, as the stores express it — a number and a unit, not a duration. + /// + [DataContract] + [Preserve] + public sealed class AdaptySubscriptionPeriod { + private AdaptySubscriptionPeriod() { } + + /// + /// The unit the period is counted in. + /// + [DataMember(Name = "unit", IsRequired = true)] public readonly AdaptySubscriptionPeriodUnit Unit; + /// + /// How many of that unit — three months is Month and 3. + /// + [DataMember(Name = "number_of_units", IsRequired = true)] public readonly long NumberOfUnits; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Unit)}: {Unit}, " + $"{nameof(NumberOfUnits)}: {NumberOfUnits}"; diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPeriodUnit.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPeriodUnit.cs index 2c3a2a8..2773a54 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPeriodUnit.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPeriodUnit.cs @@ -1,18 +1,38 @@ -// -// AdaptySubscriptionUnit.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// + /// The unit a subscription period is counted in. + /// + [Preserve] public enum AdaptySubscriptionPeriodUnit { - Day, - Week, - Month, - Year, - Unknown + /// + /// Days. + /// + [EnumMember(Value = "day")] + Day = 0, + /// + /// Weeks. + /// + [EnumMember(Value = "week")] + Week = 1, + /// + /// Months. + /// + [EnumMember(Value = "month")] + Month = 2, + /// + /// Years. + /// + [EnumMember(Value = "year")] + Year = 3, + /// + /// The store reported a unit the contract does not list. One of the two enums that keep an unknown value, because the contract lists "unknown" among theirs. + /// + [EnumMember(Value = "unknown")] + Unknown = 4 } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPhase.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPhase.cs index 4c8ae7e..f33dd79 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPhase.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionPhase.cs @@ -1,35 +1,61 @@ -// -// AdaptySubscriptionPhase.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 11.09.2023. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptySubscriptionPhase + /// + /// One stretch of an offer at one price: a free trial, then a discounted period, then the rest. + /// + [DataContract] + [Preserve] + public sealed class AdaptySubscriptionPhase { + private AdaptySubscriptionPhase() { } + + /// + /// What the phase costs. Zero for a free trial. + /// + [DataMember(Name = "price", IsRequired = true)] public readonly AdaptyPrice Price; + /// /// An integer that indicates the number of periods the product discount is available. + /// + [DataMember(Name = "number_of_periods", IsRequired = true)] public readonly int NumberOfPeriods; + /// /// The payment mode for this product discount. + /// + [DataMember(Name = "payment_mode", IsRequired = true)] public readonly AdaptyPaymentMode PaymentMode; + /// /// A [Adapty.Period] object that defines the period for the product discount. + /// + [DataMember(Name = "subscription_period", IsRequired = true)] public readonly AdaptySubscriptionPeriod SubscriptionPeriod; + /// /// The formatted subscription period of the discount for the user's localization. /// /// [Nullable] + /// + [DataMember(Name = "localized_subscription_period")] public readonly string LocalizedSubscriptionPeriod; + /// /// The formatted number of periods of the discount for the user's localization. /// /// [Nullable] + /// + [DataMember(Name = "localized_number_of_periods")] public readonly string LocalizedNumberOfPeriods; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Price)}: {Price}, " + $"{nameof(SubscriptionPeriod)}: {SubscriptionPeriod}, " + $"{nameof(NumberOfPeriods)}: {NumberOfPeriods}, " + @@ -37,4 +63,4 @@ public override string ToString() => $"{nameof(Price)}: {Price}, " + $"{nameof(LocalizedSubscriptionPeriod)}: {LocalizedSubscriptionPeriod}, " + $"{nameof(LocalizedNumberOfPeriods)}: {LocalizedNumberOfPeriods}"; } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionRenewalType.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionRenewalType.cs index 50c005f..025ef36 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionRenewalType.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionRenewalType.cs @@ -1,15 +1,23 @@ -// -// AdaptySubscriptionRenewalType.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 07.09.2023. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// + /// Android only. Whether the subscription renews by itself. + /// + [Preserve] public enum AdaptySubscriptionRenewalType { - Prepaid, - Autorenewable, + /// + /// A prepaid plan: paid for a fixed span and not renewed unless the user tops it up. + /// + [EnumMember(Value = "prepaid")] + Prepaid = 0, + /// + /// Renews on its own until cancelled. The default for the contract. + /// + [EnumMember(Value = "autorenewable")] + Autorenewable = 1, } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionUpdateParameters.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionUpdateParameters.cs index 49b2086..53e1881 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionUpdateParameters.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionUpdateParameters.cs @@ -1,21 +1,39 @@ -// -// AdaptySubscriptionUpdateParameters.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// - using System; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptySubscriptionUpdateParameters + /// + /// Android only. Turns a purchase into an upgrade or downgrade of a subscription the user + /// already has, rather than a new one. Pass it through + /// ; iOS handles the change + /// itself through the subscription group and ignores this. + /// + [DataContract] + [Preserve] + public sealed class AdaptySubscriptionUpdateParameters { - /// The product id for current subscription to change. + /// + /// The Google Play product id of the subscription being replaced. Required. + /// + [DataMember(Name = "old_sub_vendor_product_id", IsRequired = true)] public string OldSubVendorProductId; + /// + /// When the change takes effect and how the remaining time is credited. Required. + /// + [DataMember(Name = "replacement_mode", IsRequired = true)] public AdaptySubscriptionUpdateReplacementMode ReplacementMode; + /// + /// The Google Play product id of the subscription being replaced. + /// + /// + /// Describes the subscription this purchase replaces. + /// + /// When the change takes effect. + /// is null. public AdaptySubscriptionUpdateParameters( string oldSubVendorProductId, AdaptySubscriptionUpdateReplacementMode replacementMode @@ -23,10 +41,14 @@ AdaptySubscriptionUpdateReplacementMode replacementMode { OldSubVendorProductId = oldSubVendorProductId - ?? throw new ArgumentNullException(nameof(oldSubVendorProductId)); //TODO + ?? throw new ArgumentNullException(nameof(oldSubVendorProductId)); ReplacementMode = replacementMode; } + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(OldSubVendorProductId)}: {OldSubVendorProductId}, " + $"{nameof(ReplacementMode)}: {ReplacementMode}"; diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionUpdateReplacementMode.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionUpdateReplacementMode.cs index 186d018..c102809 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionUpdateReplacementMode.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptySubscriptionUpdateReplacementMode.cs @@ -1,18 +1,41 @@ -// -// AdaptySubscriptionUpdateReplacementMode.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// + /// Android only. When a subscription change takes effect and what happens to the time the user has already paid for. + /// + /// + /// These are Google Play's replacement modes; see its documentation for which ones a given change allows. + /// + [Preserve] public enum AdaptySubscriptionUpdateReplacementMode { - WithTimeProration, - ChargeProratedPrice, - WithoutProration, - Deferred, - ChargeFullPrice, + /// + /// The change is immediate and the remaining time is credited as time: the next billing date moves to pay for what is left. Google Play's WITH_TIME_PRORATION. + /// + [EnumMember(Value = "with_time_proration")] + WithTimeProration = 0, + /// + /// The change is immediate and the user is charged the difference for the rest of the current period. The billing date does not move. Only for an upgrade. Google Play's CHARGE_PRORATED_PRICE. + /// + [EnumMember(Value = "charge_prorated_price")] + ChargeProratedPrice = 1, + /// + /// The change is immediate and nothing is credited or charged until the next billing date, which does not move. Google Play's WITHOUT_PRORATION. + /// + [EnumMember(Value = "without_proration")] + WithoutProration = 2, + /// + /// The change waits for the next billing date; until then the user keeps what they had. Google Play's DEFERRED. + /// + [EnumMember(Value = "deferred")] + Deferred = 3, + /// + /// The change is immediate and the user is charged the full price of the new plan at once, starting a new billing period. Google Play's CHARGE_FULL_PRICE. + /// + [EnumMember(Value = "charge_full_price")] + ChargeFullPrice = 4, } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUICreateViewParameters.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUICreateViewParameters.cs index cd45c73..33c24c1 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUICreateViewParameters.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUICreateViewParameters.cs @@ -1,16 +1,22 @@ -// -// AdaptyUICreateFlowViewParameters.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 18.12.2024. -// - using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyUICreateFlowViewParameters + /// + /// The optional extras of : which localization to render, + /// how long to wait, and the tags, timers and assets the flow substitutes into its layout. + /// + /// + /// A dictionary handed to a setter is copied, so writing into your own copy afterwards does + /// not change what the view is built with. + /// + [DataContract] + [Preserve] + public sealed class AdaptyUICreateFlowViewParameters { /// /// The identifier of the localization to render the flow with, e.g. "en", "es", "fr". @@ -20,27 +26,118 @@ public partial class AdaptyUICreateFlowViewParameters /// A flow is localized when its view is built, not when the flow is fetched — this is the only place /// that selects the localization. Requires the native iOS 4.0.2 / Android 4.0.1 SDKs or newer. /// + [DataMember(Name = "locale")] public string Locale; + /// + /// How long to wait for the flow's assets before giving up. Null leaves the native + /// default. + /// public TimeSpan? LoadTimeout; + + /// + /// The contract carries the timeout in seconds, not as a duration literal. + /// + [DataMember(Name = "load_timeout")] + [Preserve] + private double? LoadTimeoutInSeconds => LoadTimeout?.TotalSeconds; + + /// + /// Fetches the flow's products while the view is being built, so the first frame already + /// has prices. Null leaves the native default. + /// + [DataMember(Name = "preload_products")] public bool? PreloadProducts; - public Dictionary CustomTags; - public Dictionary CustomTimers; - public Dictionary CustomAssets; + + [DataMember(Name = "custom_tags")] + private Dictionary _CustomTags; + + /// + /// The values the flow substitutes for its custom tags, keyed by tag name. Null when none were set. + /// + [Preserve] + public IReadOnlyDictionary CustomTags => + _CustomTags is null ? null : new ReadOnlyDictionary(_CustomTags); + + [DataMember(Name = "custom_timers")] + private Dictionary _CustomTimers; + + /// + /// When each of the flow's custom timers ends, keyed by timer name. A value with no of its own is read as the user's local clock. Null when none were set. + /// + [Preserve] + public IReadOnlyDictionary CustomTimers => + _CustomTimers is null ? null : new ReadOnlyDictionary(_CustomTimers); + + [DataMember(Name = "custom_assets")] + private Dictionary _CustomAssets; + + /// + /// The assets the flow uses in place of its own, keyed by the asset id in the layout. Null when none were set. + /// + [Preserve] + public IReadOnlyDictionary CustomAssets => + _CustomAssets is null ? null : new ReadOnlyDictionary(_CustomAssets); /// /// Android only. Purchase parameters applied to the products the flow offers. Ignored on iOS. /// - public Dictionary< + private Dictionary< AdaptyProductIdentifier, AdaptyPurchaseParameters - > ProductPurchaseParameters; + > _ProductPurchaseParameters; /// - /// Android only. When false, the flow view is laid out without safe area paddings. Defaults to true. + /// Android only. The purchase extras to apply to each product the flow offers, keyed by + /// identifier. Null when none were set; ignored on iOS. /// + [Preserve] + public IReadOnlyDictionary< + AdaptyProductIdentifier, + AdaptyPurchaseParameters + > ProductPurchaseParameters => + _ProductPurchaseParameters is null + ? null + : new ReadOnlyDictionary( + _ProductPurchaseParameters + ); + + /// + /// The contract keys these by adapty_product_id — neither the composite identifier + /// the app passes nor the store's own vendor_product_id. Swapping in either of those + /// compiles and sends a well-formed request whose parameters match no product. + /// + [DataMember(Name = "product_purchase_parameters")] + [Preserve] + private Dictionary ProductPurchaseParametersForRequest + { + get + { + if (_ProductPurchaseParameters is null) + { + return null; + } + + var result = new Dictionary(); + foreach (var entry in _ProductPurchaseParameters) + { + result[entry.Key._AdaptyProductId] = entry.Value; + } + return result; + } + } + + /// + /// Android only. Lays the view out without safe area paddings when false. Null leaves the + /// native default, which is true. Ignored on iOS. + /// + [DataMember(Name = "enable_safe_area_paddings")] public bool? EnableSafeAreaPaddings; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Locale)}: {Locale}, " + $"{nameof(LoadTimeout)}: {LoadTimeout}, " @@ -51,56 +148,112 @@ public override string ToString() => + $"{nameof(ProductPurchaseParameters)}: {ProductPurchaseParameters}, " + $"{nameof(EnableSafeAreaPaddings)}: {EnableSafeAreaPaddings}"; + /// + /// Sets . + /// + /// The localization to render the flow with, such as "en" or "es". public AdaptyUICreateFlowViewParameters SetLocale(string locale) { Locale = locale; return this; } + /// + /// Sets . + /// + /// How long to wait for the flow's assets. public AdaptyUICreateFlowViewParameters SetLoadTimeout(TimeSpan? loadTimeout) { LoadTimeout = loadTimeout; return this; } + /// + /// Sets . + /// + /// True to fetch the products while the view is built. public AdaptyUICreateFlowViewParameters SetPreloadProducts(bool? preloadProducts) { PreloadProducts = preloadProducts; return this; } + /// + /// Sets , copying the dictionary. + /// + /// The value for each custom tag, keyed by tag name. public AdaptyUICreateFlowViewParameters SetCustomTags( - Dictionary customTags + IReadOnlyDictionary customTags ) { - CustomTags = customTags; + _CustomTags = Copy(customTags); return this; } + // Copied, so a caller that keeps writing to its own dictionary after handing it over does + // not change what the view will be built with. + private static Dictionary Copy( + IReadOnlyDictionary source + ) + { + if (source is null) + { + return null; + } + + var copy = new Dictionary(); + foreach (var entry in source) + { + copy[entry.Key] = entry.Value; + } + return copy; + } + + /// + /// When each timer ends. A with no of its + /// own is read as the user's local clock, so + /// new DateTime(2026, 7, 30, 22, 0, 0) means 22:00 where the user is; pass a + /// value to mean 22:00 UTC. + /// + /// + /// Sets , copying the dictionary. + /// public AdaptyUICreateFlowViewParameters SetCustomTimers( - Dictionary customTimers + IReadOnlyDictionary customTimers ) { - CustomTimers = customTimers; + _CustomTimers = Copy(customTimers); return this; } + /// + /// Sets , copying the dictionary. + /// + /// The asset to use for each id in the layout. public AdaptyUICreateFlowViewParameters SetCustomAssets( - Dictionary customAssets + IReadOnlyDictionary customAssets ) { - CustomAssets = customAssets; + _CustomAssets = Copy(customAssets); return this; } + /// + /// Sets , copying the dictionary. Android only. + /// + /// The purchase extras for each product. public AdaptyUICreateFlowViewParameters SetProductPurchaseParameters( - Dictionary productPurchaseParameters + IReadOnlyDictionary productPurchaseParameters ) { - ProductPurchaseParameters = productPurchaseParameters; + _ProductPurchaseParameters = Copy(productPurchaseParameters); return this; } + /// + /// Sets . Android only. + /// + /// False to lay the view out without safe area paddings. public AdaptyUICreateFlowViewParameters SetEnableSafeAreaPaddings( bool? enableSafeAreaPaddings ) diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIDialogActionType.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIDialogActionType.cs index 88e9096..4b50b34 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIDialogActionType.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIDialogActionType.cs @@ -1,14 +1,23 @@ -// -// AdaptyUIDialogActionType.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2024. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public enum AdaptyUIDialogActionType { - Primary, - Secondary + /// + /// Which button closed a dialog shown by . + /// + [Preserve] + public enum AdaptyUIDialogActionType + { + /// + /// The default action — the title given as the default one. + /// + [EnumMember(Value = "primary")] + Primary = 0, + /// + /// The other action. Only reported when the dialog was configured with one. + /// + [EnumMember(Value = "secondary")] + Secondary = 1, } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIDialogConfiguration.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIDialogConfiguration.cs index 38ff951..20926c6 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIDialogConfiguration.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIDialogConfiguration.cs @@ -1,34 +1,44 @@ -// -// AdaptyUIDialogConfiguration.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 07.09.2023. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyUIDialogConfiguration + /// + /// What puts on the dialog. Only the default action title + /// is required; a dialog without a secondary one has a single button. + /// + [DataContract] + [Preserve] + public sealed class AdaptyUIDialogConfiguration { - /// + /// /// The title of the dialog. /// + [DataMember(Name = "title")] public string Title; /// /// Descriptive text that provides additional details about the reason for the dialog. /// + [DataMember(Name = "content")] public string Content; /// /// The action title to display as part of the dialog. If you provide two actions, be sure the `defaultAction` cancels the operation and leaves things unchanged. /// + [DataMember(Name = "default_action_title", IsRequired = true)] public string DefaultActionTitle; /// /// The secondary action title to display as part of the dialog. /// + [DataMember(Name = "secondary_action_title")] public string SecondaryActionTitle; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Title)}: {Title}, " + $"{nameof(Content)}: {Content}, " + @@ -36,24 +46,40 @@ public override string ToString() => $"{nameof(SecondaryActionTitle)}: {SecondaryActionTitle}"; + /// + /// Sets the title. + /// + /// The dialog's title. public AdaptyUIDialogConfiguration SetTitle(string title) { Title = title; return this; } + /// + /// Sets the content. + /// + /// The body text. public AdaptyUIDialogConfiguration SetContent(string content) { Content = content; return this; } + /// + /// Sets the default action title. + /// + /// The label of the button reported as . public AdaptyUIDialogConfiguration SetDefaultActionTitle(string defaultActionTitle) { DefaultActionTitle = defaultActionTitle; return this; } + /// + /// Sets the secondary action title. + /// + /// The label of the button reported as . Leave it out for a one-button dialog. public AdaptyUIDialogConfiguration SetSecondaryActionTitle(string secondaryActionTitle) { SecondaryActionTitle = secondaryActionTitle; @@ -61,4 +87,4 @@ public AdaptyUIDialogConfiguration SetSecondaryActionTitle(string secondaryActio } } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIFlowView.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIFlowView.cs index ff5e4c4..f3a463e 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIFlowView.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIFlowView.cs @@ -1,14 +1,32 @@ -// -// AdaptyUIFlowView.cs -// AdaptySDK -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyUIFlowView + /// + /// A flow view built by . Single use: once dismissed it + /// is destroyed, and showing the flow again means building another one. + /// + [DataContract] + [Preserve] + public sealed class AdaptyUIFlowView { + private AdaptyUIFlowView() { } + + /// + /// The identifier of this view, which the events carry back. + /// + [DataMember(Name = "id", IsRequired = true)] public string Id; + /// + /// The placement the flow behind this view was fetched for. + /// + [DataMember(Name = "placement_id", IsRequired = true)] public string PlacementId; + /// + /// The variation the flow resolved to. Purchases made here are attributed to it. + /// + [DataMember(Name = "variation_id", IsRequired = true)] public string VariationId; /// @@ -19,8 +37,13 @@ public partial class AdaptyUIFlowView /// localization exists, and the flow's default localization otherwise. It is null when the native SDK is /// older than iOS 4.0.2 / Android 4.0.1 and does not report it. /// + [DataMember(Name = "locale")] public string Locale; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Id)}: {Id}, " + $"{nameof(PlacementId)}: {PlacementId}, " diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIIOSPresentationStyle.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIIOSPresentationStyle.cs index 4f41f49..7138cb8 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIIOSPresentationStyle.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIIOSPresentationStyle.cs @@ -1,15 +1,23 @@ -// -// AdaptyUIIOSPresentationStyle.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2024. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { + /// + /// iOS only. How a flow view is presented. Ignored on Android. + /// + [Preserve] public enum AdaptyUIIOSPresentationStyle { - FullScreen, - PageSheet, + /// + /// Covers the screen. + /// + [EnumMember(Value = "full_screen")] + FullScreen = 0, + /// + /// A sheet over the current screen, which the user can swipe down. + /// + [EnumMember(Value = "page_sheet")] + PageSheet = 1, } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIMediaCacheConfiguration.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIMediaCacheConfiguration.cs index d8891ad..a758c1b 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIMediaCacheConfiguration.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIMediaCacheConfiguration.cs @@ -1,18 +1,38 @@ -// -// AdaptyUIMediaCacheConfiguration.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 07.09.2023. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace AdaptySDK { - public partial class AdaptyUIMediaCacheConfiguration + /// + /// Limits for the cache the flow renderer keeps for images and video. Any limit left null + /// keeps the native default. + /// + [DataContract] + [Preserve] + public sealed class AdaptyUIMediaCacheConfiguration { + /// + /// How much the in-memory cache may hold, in bytes. + /// + [DataMember(Name = "memory_storage_total_cost_limit")] public int? MemoryStorageTotalCostLimit; + /// + /// How many items the in-memory cache may hold. + /// + [DataMember(Name = "memory_storage_count_limit")] public int? MemoryStorageCountLimit; + /// + /// How much the on-disk cache may hold, in bytes. + /// + [DataMember(Name = "disk_storage_size_limit")] public int? DiskStorageSizeLimit; + /// + /// Sets the cache limits. Any of them null keeps the native default. + /// + /// In-memory cache limit, in bytes. + /// How many items the in-memory cache may hold. + /// On-disk cache limit, in bytes. public AdaptyUIMediaCacheConfiguration(int? memoryStorageTotalCostLimit, int? memoryStorageCountLimit, int? diskStorageSizeLimit) { MemoryStorageTotalCostLimit = memoryStorageTotalCostLimit; @@ -20,10 +40,14 @@ public AdaptyUIMediaCacheConfiguration(int? memoryStorageTotalCostLimit, int? me DiskStorageSizeLimit = diskStorageSizeLimit; } + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(MemoryStorageTotalCostLimit)}: {MemoryStorageTotalCostLimit}, " + $"{nameof(MemoryStorageCountLimit)}: {MemoryStorageCountLimit}, " + $"{nameof(DiskStorageSizeLimit)}: {DiskStorageSizeLimit}"; } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIUserAction.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIUserAction.cs index ea158da..160d6b3 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIUserAction.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIUserAction.cs @@ -1,16 +1,43 @@ -// -// AdaptyUIUserAction.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 17.12.2024. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; -namespace AdaptySDK { - public partial class AdaptyUIUserAction { +namespace AdaptySDK +{ + /// + /// Something the user did in a flow view, reported through + /// FlowViewDidPerformAction. Nothing is done for you — a close does not dismiss the + /// view. + /// + [DataContract] + [Preserve] + public sealed class AdaptyUIUserAction + { + private AdaptyUIUserAction() { } + + /// + /// Which action it was. + /// + [DataMember(Name = "type", IsRequired = true)] public AdaptyUIUserActionType Type; + + /// + /// What the action carries: the URL for , the flow's + /// own identifier for . Null for the rest. + /// + [DataMember(Name = "value")] public string Value; + + /// + /// Where the flow asked for the URL to open. Set for + /// only. + /// + [DataMember(Name = "open_in")] public AdaptyWebPresentation? OpenIn; + /// + /// A description for logs and the debugger. The format is not part of the contract — + /// read the members rather than parsing it. + /// public override string ToString() => $"{nameof(Type)}: {Type}, " + $"{nameof(Value)}: {Value}, " + diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIUserActionType.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIUserActionType.cs index 02fc9f9..1f11924 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIUserActionType.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIUserActionType.cs @@ -1,15 +1,33 @@ -// -// AdaptyUIUserActionType+JSON.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 17.12.2024. -// +using System.Runtime.Serialization; +using UnityEngine.Scripting; -namespace AdaptySDK { - public enum AdaptyUIUserActionType { - Close, - SystemBack, - OpenUrl, - Custom +namespace AdaptySDK +{ + /// + /// What the user did in a flow view, as reported by FlowViewDidPerformAction. + /// + [Preserve] + public enum AdaptyUIUserActionType + { + /// + /// The close control of the flow was tapped. The view is not dismissed for you — call if that is what you want. + /// + [EnumMember(Value = "close")] + Close = 0, + /// + /// Android only. The system back button was pressed. Handled the same way as a close: the view stays until you dismiss it. + /// + [EnumMember(Value = "system_back")] + SystemBack = 1, + /// + /// A link in the flow was tapped; the URL is on the action. + /// + [EnumMember(Value = "open_url")] + OpenUrl = 2, + /// + /// An action the flow defines itself was triggered; its identifier is on the action. + /// + [EnumMember(Value = "custom")] + Custom = 3, } -} \ No newline at end of file +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyWebPresentation.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyWebPresentation.cs index bdc3d39..fc0f2a8 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyWebPresentation.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyWebPresentation.cs @@ -1,23 +1,24 @@ -// -// AdaptyWebPresentation.cs -// AdaptySDK -// - -namespace AdaptySDK -{ - /// - /// Controls how web content (paywalls, external URLs in onboarding) is presented. - /// - public enum AdaptyWebPresentation - { - /// - /// Open in the default external browser (outside the app). - /// - ExternalBrowser, - - /// - /// Open in an in-app browser/web view. - /// - InAppBrowser, - } -} +using System.Runtime.Serialization; +using UnityEngine.Scripting; + +namespace AdaptySDK +{ + /// + /// Where a web paywall opens. + /// + [Preserve] + public enum AdaptyWebPresentation + { + /// + /// The device's browser app, leaving your app. + /// + [EnumMember(Value = "browser_out_app")] + ExternalBrowser = 0, + + /// + /// A browser presented over your app, which stays in the foreground. + /// + [EnumMember(Value = "browser_in_app")] + InAppBrowser = 1, + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyWebPresentation.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyWebPresentation.cs.meta index 827c439..7286ce1 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyWebPresentation.cs.meta +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyWebPresentation.cs.meta @@ -1,2 +1,2 @@ -fileFormatVersion: 2 -guid: a1b2c3d4e5f64789a0b1c2d3e4f5a6b7 +fileFormatVersion: 2 +guid: a1b2c3d4e5f64789a0b1c2d3e4f5a6b7 diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AppTrackingTransparencyStatus.cs b/Packages/com.adapty.unity-sdk/Runtime/Models/AppTrackingTransparencyStatus.cs index fe076c1..e3f8542 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AppTrackingTransparencyStatus.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Models/AppTrackingTransparencyStatus.cs @@ -1,17 +1,33 @@ -// -// AppTrackingTransparencyStatus.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 20.12.2022. -// +using UnityEngine.Scripting; -namespace AdaptySDK { +namespace AdaptySDK +{ - public enum AppTrackingTransparencyStatus { - NotDetermined, - Restricted, - Denied, - Authorized + /// + /// iOS only. What the user answered to the App Tracking Transparency prompt, as ATTrackingManager.AuthorizationStatus reports it. + /// + /// + /// The numbers are Apple's own, so a value read from ATTrackingManager can be cast across directly. + /// + [Preserve] + public enum AppTrackingTransparencyStatus + { + /// + /// The prompt has not been shown yet. + /// + NotDetermined = 0, + /// + /// Tracking is not permitted on this device — a restriction outside the user's control, such as a managed device. + /// + Restricted = 1, + /// + /// The user was asked and declined. + /// + Denied = 2, + /// + /// The user was asked and agreed. + /// + Authorized = 3 } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete.meta new file mode 100644 index 0000000..06c1aa7 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a0b965748a364654804b865aaedbc9d1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Events.Obsolete.cs b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Events.Obsolete.cs new file mode 100644 index 0000000..e17b529 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Events.Obsolete.cs @@ -0,0 +1,221 @@ +using System; +using UnityEngine; +using AdaptySDK.Serialization; +using Newtonsoft.Json.Linq; + +namespace AdaptySDK +{ + public static partial class Adapty + { + [Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] + private static IAdaptyOnboardingsEventsListener m_OnboardingsEventsListener; + + // Its own callback rather than a line in the live one: a reference from live code to an + // obsolete member would raise CS0618 where there is nothing for the caller to act on. + [Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + internal static void ResetOnboardingsListener() => m_OnboardingsEventsListener = null; + + /// + /// Sets the event listener for onboarding view events. + /// + /// The implementation to receive events, or null to detach the previous one. + [Obsolete( + "The legacy onboarding API is deprecated in favor of Flows. Use SetFlowsEventsListener instead." + )] + public static void SetOnboardingsEventsListener(IAdaptyOnboardingsEventsListener listener) + { + m_OnboardingsEventsListener = listener; + } + + [Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] + private static bool RequireOnboardingsListener(string eventId) + { + if (m_OnboardingsEventsListener == null) + { + Debug.LogWarning( + string.Format( + "[Adapty] Onboardings events listener is not set, ignoring event '{0}'. Call Adapty.SetOnboardingsEventsListener() to receive onboarding events.", + eventId + ) + ); + return false; + } + return true; + } + + /// + /// Dispatches the events of the legacy onboarding API. + /// + /// + /// Split out of so that the deprecation warnings it raises stay on + /// this one method instead of on every case of the main switch. + /// + [Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] + private static void OnLegacyOnboardingMessage(string id, JObject parameters) + { + switch (id) + { + case "onboarding_did_fail_with_error": + { + if (!RequireOnboardingsListener(id)) + return; + var view = Required(parameters, "view"); + var error = Required(parameters, "error"); + try + { + m_OnboardingsEventsListener.OnboardingViewDidFailWithError(view, error); + } + catch (Exception e) + { + throw new Exception( + "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewDidFailWithError(..)", + e + ); + } + return; + } + case "onboarding_on_analytics_action": + { + if (!RequireOnboardingsListener(id)) + return; + var view = Required(parameters, "view"); + var meta = Required(parameters, "meta"); + var ev = Required(parameters, "event"); + try + { + m_OnboardingsEventsListener.OnboardingViewOnAnalyticsEvent(view, meta, ev); + } + catch (Exception e) + { + throw new Exception( + "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewOnAnalyticsEvent(..)", + e + ); + } + return; + } + case "onboarding_did_finish_loading": + { + if (!RequireOnboardingsListener(id)) + return; + var view = Required(parameters, "view"); + var meta = Required(parameters, "meta"); + try + { + m_OnboardingsEventsListener.OnboardingViewDidFinishLoading(view, meta); + } + catch (Exception e) + { + throw new Exception( + "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewDidFinishLoading(..)", + e + ); + } + return; + } + case "onboarding_on_close_action": + { + if (!RequireOnboardingsListener(id)) + return; + var view = Required(parameters, "view"); + var meta = Required(parameters, "meta"); + var actionId = Required(parameters, "action_id"); + try + { + m_OnboardingsEventsListener.OnboardingViewOnCloseAction( + view, + meta, + actionId + ); + } + catch (Exception e) + { + throw new Exception( + "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewOnCloseAction(..)", + e + ); + } + return; + } + case "onboarding_on_paywall_action": + { + if (!RequireOnboardingsListener(id)) + return; + var view = Required(parameters, "view"); + var meta = Required(parameters, "meta"); + var actionId = Required(parameters, "action_id"); + try + { + m_OnboardingsEventsListener.OnboardingViewOnPaywallAction( + view, + meta, + actionId + ); + } + catch (Exception e) + { + throw new Exception( + "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewOnPaywallAction(..)", + e + ); + } + return; + } + case "onboarding_on_custom_action": + { + if (!RequireOnboardingsListener(id)) + return; + var view = Required(parameters, "view"); + var meta = Required(parameters, "meta"); + var actionId = Required(parameters, "action_id"); + try + { + m_OnboardingsEventsListener.OnboardingViewOnCustomAction( + view, + meta, + actionId + ); + } + catch (Exception e) + { + throw new Exception( + "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewOnCustomAction(..)", + e + ); + } + return; + } + case "onboarding_on_state_updated_action": + { + if (!RequireOnboardingsListener(id)) + return; + var view = Required(parameters, "view"); + var meta = Required(parameters, "meta"); + var elementId = AdaptyJsonRequire.String( + AdaptyJsonRequire.Object(parameters, "action"), + "element_id" + ); + var @params = Required(parameters, "action"); + try + { + m_OnboardingsEventsListener.OnboardingViewOnStateUpdatedAction( + view, + meta, + elementId, + @params + ); + } + catch (Exception e) + { + throw new Exception( + "Failed to invoke IAdaptyOnboardingsEventsListener.OnboardingViewOnStateUpdatedAction(..)", + e + ); + } + return; + } + } + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Events.Obsolete.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Events.Obsolete.cs.meta new file mode 100644 index 0000000..ad8ae55 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Events.Obsolete.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 68473b89d79241dd8e0c60c44769e246 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Obsolete.cs b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Obsolete.cs new file mode 100644 index 0000000..abd9035 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Obsolete.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using AdaptySDK.Serialization; +using Newtonsoft.Json.Linq; + +namespace AdaptySDK +{ + public static partial class Adapty + { + /// + /// Adapty allows you remotely configure onboarding screens that will be displayed in your app. + /// This way you don't have to hardcode the onboarding content and can dynamically change it or run A/B tests without app releases. + /// + /// + /// Read more at Adapty Documentation + /// + /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. + /// The identifier of the onboarding localization. + /// By default SDK will try to load data from server and will return cached data in case of failure. Otherwise use `.returnCacheDataElseLoad` to return cached data if it exists. + /// The timeout for the onboarding loading. + /// The action that will be called with the result. + [Obsolete("The legacy onboarding API is deprecated in favor of Flows. Use GetFlow instead.")] + public static void GetOnboarding( + string placementId, + string locale, + AdaptyPlacementFetchPolicy fetchPolicy, + TimeSpan? loadTimeout, + Action completionHandler + ) + { + var parameters = new JObject(); + + parameters["placement_id"] = placementId; + + if (locale != null) + { + parameters["locale"] = locale; + } + + if (fetchPolicy != null) + { + parameters["fetch_policy"] = AdaptyJson.ToNode(fetchPolicy); + } + + if (loadTimeout.HasValue) + { + parameters["load_timeout"] = loadTimeout.Value.TotalSeconds; + } + + AdaptyRequest.Send("get_onboarding", parameters, completionHandler); + } + + /// + /// This method enables you to retrieve the onboarding from the Default Audience without having to wait for the Adapty SDK to send all the user information required for segmentation to the server. + /// + /// + /// Read more at Adapty Documentation + /// + /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. + /// The identifier of the onboarding localization. + /// By default SDK will try to load data from server and will return cached data in case of failure. Otherwise use `.returnCacheDataElseLoad` to return cached data if it exists. + /// The action that will be called with the result. + [Obsolete( + "The legacy onboarding API is deprecated in favor of Flows. Use GetFlowForDefaultAudience instead." + )] + public static void GetOnboardingForDefaultAudience( + string placementId, + string locale, + AdaptyPlacementFetchPolicy fetchPolicy, + Action completionHandler + ) + { + var parameters = new JObject(); + parameters["placement_id"] = placementId; + + if (locale != null) + { + parameters["locale"] = locale; + } + + if (fetchPolicy != null) + { + parameters["fetch_policy"] = AdaptyJson.ToNode(fetchPolicy); + } + + AdaptyRequest.Send("get_onboarding_for_default_audience", parameters, completionHandler); + } + + /// + /// This method enables you to retrieve the onboarding from the Default Audience without having to wait for the Adapty SDK to send all the user information required for segmentation to the server. + /// + /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. + /// The identifier of the onboarding localization. + /// The action that will be called with the result. + [Obsolete( + "The legacy onboarding API is deprecated in favor of Flows. Use GetFlowForDefaultAudience instead." + )] + public static void GetOnboardingForDefaultAudience( + string placementId, + string locale, + Action completionHandler + ) => GetOnboardingForDefaultAudience(placementId, locale, null, completionHandler); + + /// + /// This method enables you to retrieve the onboarding from the Default Audience without having to wait for the Adapty SDK to send all the user information required for segmentation to the server. + /// + /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. + /// By default SDK will try to load data from server and will return cached data in case of failure. Otherwise use `.returnCacheDataElseLoad` to return cached data if it exists. + /// The action that will be called with the result. + [Obsolete( + "The legacy onboarding API is deprecated in favor of Flows. Use GetFlowForDefaultAudience instead." + )] + public static void GetOnboardingForDefaultAudience( + string placementId, + AdaptyPlacementFetchPolicy fetchPolicy, + Action completionHandler + ) => GetOnboardingForDefaultAudience(placementId, null, fetchPolicy, completionHandler); + + /// + /// This method enables you to retrieve the onboarding from the Default Audience without having to wait for the Adapty SDK to send all the user information required for segmentation to the server. + /// + /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. + /// The action that will be called with the result. + [Obsolete( + "The legacy onboarding API is deprecated in favor of Flows. Use GetFlowForDefaultAudience instead." + )] + public static void GetOnboardingForDefaultAudience( + string placementId, + Action completionHandler + ) => GetOnboardingForDefaultAudience(placementId, null, null, completionHandler); + + /// + /// Adapty allows you remotely configure onboarding screens that will be displayed in your app. + /// + /// The identifier of the desired placement. This is the value you specified when you created the placement in the Adapty Dashboard. + /// The action that will be called with the result. + [Obsolete("The legacy onboarding API is deprecated in favor of Flows. Use GetFlow instead.")] + public static void GetOnboarding( + string placementId, + Action completionHandler + ) => GetOnboarding(placementId, null, null, null, completionHandler); + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Obsolete.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Obsolete.cs.meta new file mode 100644 index 0000000..5a9c530 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Adapty.Obsolete.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6296e23e269045b099cf03bb2e41226c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/AdaptyUI.Obsolete.cs b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/AdaptyUI.Obsolete.cs new file mode 100644 index 0000000..755a3e6 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/AdaptyUI.Obsolete.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using AdaptySDK.Serialization; +using Newtonsoft.Json.Linq; + +namespace AdaptySDK +{ + public static partial class AdaptyUI + { + /// + /// Creates an onboarding view from an AdaptyOnboarding object. + /// + /// + /// Right after receiving an , you can create the corresponding to present it afterwards. + /// Read more at Adapty Documentation + /// + /// An object for which you are trying to create a view. + /// Controls how external URLs are presented in the onboarding (in-app browser vs external browser). Default is . + /// The action that will be called with the result. The result contains an object. + [Obsolete( + "The legacy onboarding API is deprecated in favor of Flows. Use CreateFlowView instead." + )] + public static void CreateOnboardingView( + AdaptyOnboarding onboarding, + AdaptyWebPresentation externalUrlsPresentation, + Action completionHandler + ) + { + var parameters = new JObject(); + parameters["onboarding"] = AdaptyJson.ToNode(onboarding); + parameters["external_urls_presentation"] = AdaptyJson.ToNode(externalUrlsPresentation); + + AdaptyRequest.Send("adapty_ui_create_onboarding_view", parameters, completionHandler); + } + + /// + /// Presents the onboarding view to the user. + /// + /// + /// This method presents the onboarding view using the default full-screen presentation style. + /// + /// An object representing the view to present. + /// The action that will be called with the result. + [Obsolete( + "The legacy onboarding API is deprecated in favor of Flows. Use PresentFlowView instead." + )] + public static void PresentOnboardingView( + AdaptyUIOnboardingView view, + Action completionHandler + ) + { + PresentOnboardingView(view, AdaptyUIIOSPresentationStyle.FullScreen, completionHandler); + } + + /// + /// Presents the onboarding view to the user with a specified presentation style. + /// + /// + /// This method presents the onboarding view using the specified iOS presentation style (iOS only). + /// + /// An object representing the view to present. + /// An object representing the iOS presentation style (iOS only). + /// The action that will be called with the result. + [Obsolete( + "The legacy onboarding API is deprecated in favor of Flows. Use PresentFlowView instead." + )] + public static void PresentOnboardingView( + AdaptyUIOnboardingView view, + AdaptyUIIOSPresentationStyle iosPresentationStyle, + Action completionHandler + ) + { + var parameters = new JObject(); + parameters["id"] = view.Id; + parameters["ios_presentation_style"] = AdaptyJson.ToNode(iosPresentationStyle); + + AdaptyRequest.SendVoid("adapty_ui_present_onboarding_view", parameters, completionHandler); + } + + /// + /// Dismisses the onboarding view. + /// + /// + /// Call this method when you want to dismiss the onboarding view from the screen. + /// + /// An object representing the view to dismiss. + /// The action that will be called with the result. + [Obsolete( + "The legacy onboarding API is deprecated in favor of Flows. Use DismissFlowView instead." + )] + public static void DismissOnboardingView( + AdaptyUIOnboardingView view, + Action completionHandler + ) => DismissOnboardingView(view, false, completionHandler); + + [Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] + private static void DismissOnboardingView( + AdaptyUIOnboardingView view, + bool destroy, + Action completionHandler + ) + { + var parameters = new JObject(); + parameters["id"] = view.Id; + parameters["destroy"] = destroy; + + AdaptyRequest.SendVoid("adapty_ui_dismiss_onboarding_view", parameters, completionHandler); + } + + /// + /// Presents a dialog on the onboarding view. + /// + /// + /// This method shows a dialog with custom configuration on the onboarding view. The dialog can be used for various purposes like showing terms, privacy policy, or custom messages. + /// + /// An object representing the view on which to show the dialog. + /// An object that contains the dialog configuration. + /// The action that will be called with the result. The result contains the indicating which action was taken. + [Obsolete( + "The legacy onboarding API is deprecated in favor of Flows. Use the AdaptyUIFlowView overload instead." + )] + public static void ShowDialog( + AdaptyUIOnboardingView view, + AdaptyUIDialogConfiguration configuration, + Action completionHandler + ) + { + ShowDialog(view.Id, configuration, completionHandler); + } + + /// + /// Creates an onboarding view from an AdaptyOnboarding object. + /// + /// An object for which you are trying to create a view. + /// The action that will be called with the result. The result contains an object. + [Obsolete( + "The legacy onboarding API is deprecated in favor of Flows. Use CreateFlowView instead." + )] + public static void CreateOnboardingView( + AdaptyOnboarding onboarding, + Action completionHandler + ) => + CreateOnboardingView( + onboarding, + AdaptyWebPresentation.ExternalBrowser, + completionHandler + ); + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/AdaptyUI.Obsolete.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/AdaptyUI.Obsolete.cs.meta new file mode 100644 index 0000000..e6f6015 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/AdaptyUI.Obsolete.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 825a6976dafc4c319821109f62d4123d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/IAdaptyOnboardingsEventsListener.cs b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/IAdaptyOnboardingsEventsListener.cs new file mode 100644 index 0000000..c2fa58d --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/IAdaptyOnboardingsEventsListener.cs @@ -0,0 +1,95 @@ +using System; + +namespace AdaptySDK +{ + /// + /// Interface for listening to onboarding view events. + /// + /// + /// Implement this interface to receive notifications about onboarding view lifecycle, user actions, and analytics events. + /// Use to register your listener. + /// Part of the legacy onboarding API, which is deprecated in favor of flows — see . + /// + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] + public interface IAdaptyOnboardingsEventsListener + { + /// + /// Called when the onboarding view fails with an error. + /// + /// The that failed. + /// The object describing the error. + void OnboardingViewDidFailWithError(AdaptyUIOnboardingView view, AdaptyError error); + + /// + /// Called when the onboarding view finishes loading. + /// + /// The that finished loading. + /// The object containing onboarding metadata. + void OnboardingViewDidFinishLoading( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta + ); + + /// + /// Called when a close action is triggered in the onboarding view. + /// + /// The where the action occurred. + /// The object containing onboarding metadata. + /// The identifier of the close action. + void OnboardingViewOnCloseAction( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta, + string actionId + ); + + /// + /// Called when a paywall action is triggered in the onboarding view. + /// + /// The where the action occurred. + /// The object containing onboarding metadata. + /// The identifier of the paywall action. + void OnboardingViewOnPaywallAction( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta, + string actionId + ); + + /// + /// Called when a custom action is triggered in the onboarding view. + /// + /// The where the action occurred. + /// The object containing onboarding metadata. + /// The identifier of the custom action. + void OnboardingViewOnCustomAction( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta, + string actionId + ); + + /// + /// Called when the state of an element in the onboarding view is updated. + /// + /// The where the update occurred. + /// The object containing onboarding metadata. + /// The identifier of the element whose state was updated. + /// The object containing the updated state parameters. + void OnboardingViewOnStateUpdatedAction( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta, + string elementId, + AdaptyOnboardingsStateUpdatedParams @params + ); + + /// + /// Called when an analytics event is triggered in the onboarding view. + /// + /// The where the event occurred. + /// The object containing onboarding metadata. + /// The object containing analytics event data. + void OnboardingViewOnAnalyticsEvent( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta, + AdaptyOnboardingsAnalyticsEvent analyticsEvent + ); + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/IAdaptyOnboardingsEventsListener.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/IAdaptyOnboardingsEventsListener.cs.meta new file mode 100644 index 0000000..2d49e3d --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/IAdaptyOnboardingsEventsListener.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9e3031b2abc1446a94a973cfb0716470 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models.meta new file mode 100644 index 0000000..c721609 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4a98678062db4c66ba2297b1bebe5b3d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboarding.cs b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboarding.cs similarity index 68% rename from Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboarding.cs rename to Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboarding.cs index 18211b1..10aacfd 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboarding.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboarding.cs @@ -1,15 +1,9 @@ -// -// AdaptyOnboarding.cs -// AdaptySDK -// -// Created by Aleksei Goncharov on 09.09.2025. -// - +using UnityEngine.Scripting; using System.Collections.Generic; +using System.Runtime.Serialization; namespace AdaptySDK { - using AdaptySDK.SimpleJSON; /// /// Represents an onboarding configuration in Adapty. @@ -18,26 +12,35 @@ namespace AdaptySDK /// An onboarding is a set of screens that can be displayed to users during their first app experience. /// Read more at Adapty Documentation /// - public partial class AdaptyOnboarding + [DataContract] + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] + public sealed class AdaptyOnboarding { + private AdaptyOnboarding() { } + /// /// An object that contains information about the placement of the onboarding. /// + [DataMember(Name = "placement", IsRequired = true)] public readonly AdaptyPlacement Placement; /// /// The unique identifier of the onboarding. /// + [DataMember(Name = "onboarding_id", IsRequired = true)] public readonly string OnboardingId; /// /// The onboarding name configured in the Adapty Dashboard. /// + [DataMember(Name = "onboarding_name", IsRequired = true)] public readonly string Name; /// /// The identifier of the variation, used to attribute analytics to the onboarding. /// + [DataMember(Name = "variation_id", IsRequired = true)] public readonly string VariationId; /// @@ -46,12 +49,19 @@ public partial class AdaptyOnboarding /// /// This can be null if no remote config is configured for the onboarding. /// - public readonly AdaptyRemoteConfig RemoteConfig; // nullable + [DataMember(Name = "remote_config")] + public readonly AdaptyRemoteConfig RemoteConfig; + [DataMember(Name = "onboarding_builder", IsRequired = true)] private readonly OnboardingBuilder _Builder; + [DataMember(Name = "response_created_at", IsRequired = true)] private readonly long _ResponseCreatedAt; - private readonly string _PayloadData; // nullable + + [DataMember(Name = "payload_data")] + private readonly string _PayloadData; + + [DataMember(Name = "request_locale", IsRequired = true)] private readonly string _RequestLocale; public override string ToString() => @@ -65,14 +75,13 @@ public override string ToString() => + $"{nameof(_PayloadData)}: {_PayloadData}, " + $"{nameof(_RequestLocale)}: {_RequestLocale}"; + [DataContract] private sealed class OnboardingBuilder { - public readonly string ConfigUrl; + private OnboardingBuilder() { } - internal OnboardingBuilder(string configUrl) - { - ConfigUrl = configUrl; - } + [DataMember(Name = "config_url", IsRequired = true)] + public readonly string ConfigUrl; public override string ToString() => $"{nameof(ConfigUrl)}: {ConfigUrl}"; } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboarding.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboarding.cs.meta similarity index 100% rename from Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboarding.cs.meta rename to Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboarding.cs.meta diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboardingsAnalyticsEvent.cs b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboardingsAnalyticsEvent.cs similarity index 57% rename from Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboardingsAnalyticsEvent.cs rename to Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboardingsAnalyticsEvent.cs index 50bb85e..356b90f 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboardingsAnalyticsEvent.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboardingsAnalyticsEvent.cs @@ -1,40 +1,53 @@ -// -// AdaptyOnboardingsAnalyticsEvent.cs -// AdaptySDK -// -// Created by GPT-5 on 17.09.2025. -// +using UnityEngine.Scripting; namespace AdaptySDK { + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public abstract class AdaptyOnboardingsAnalyticsEvent { } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsAnalyticsEventOnboardingStarted : AdaptyOnboardingsAnalyticsEvent { } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsAnalyticsEventScreenPresented : AdaptyOnboardingsAnalyticsEvent { } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsAnalyticsEventSecondScreenPresented : AdaptyOnboardingsAnalyticsEvent { } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsAnalyticsEventRegistrationScreenPresented : AdaptyOnboardingsAnalyticsEvent { } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsAnalyticsEventProductsScreenPresented : AdaptyOnboardingsAnalyticsEvent { } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsAnalyticsEventUserEmailCollected : AdaptyOnboardingsAnalyticsEvent { } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsAnalyticsEventOnboardingCompleted : AdaptyOnboardingsAnalyticsEvent { } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsAnalyticsEventScreenCompleted : AdaptyOnboardingsAnalyticsEvent { - public readonly string ElementId; // nullable - public readonly string Reply; // nullable + public readonly string ElementId; + public readonly string Reply; public AdaptyOnboardingsAnalyticsEventScreenCompleted(string elementId, string reply) { @@ -43,6 +56,8 @@ public AdaptyOnboardingsAnalyticsEventScreenCompleted(string elementId, string r } } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsAnalyticsEventUnknown : AdaptyOnboardingsAnalyticsEvent { public readonly string Name; diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboardingsAnalyticsEvent.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboardingsAnalyticsEvent.cs.meta similarity index 100% rename from Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboardingsAnalyticsEvent.cs.meta rename to Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboardingsAnalyticsEvent.cs.meta diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboardingsStateUpdatedParams.cs b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboardingsStateUpdatedParams.cs similarity index 73% rename from Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboardingsStateUpdatedParams.cs rename to Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboardingsStateUpdatedParams.cs index 4b98a32..beb5375 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboardingsStateUpdatedParams.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboardingsStateUpdatedParams.cs @@ -1,16 +1,14 @@ -// -// AdaptyOnboardingsStateUpdatedParams.cs -// AdaptySDK -// -// Created by GPT-5 on 17.09.2025. -// - +using UnityEngine.Scripting; using System.Collections.Generic; namespace AdaptySDK { + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public abstract class AdaptyOnboardingsStateUpdatedParams { } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsSelectParams : AdaptyOnboardingsStateUpdatedParams { public readonly string Id; @@ -28,6 +26,8 @@ public override string ToString() => $"{nameof(Id)}: {Id}, {nameof(Value)}: {Value}, {nameof(Label)}: {Label}"; } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsMultiSelectParams : AdaptyOnboardingsStateUpdatedParams { public readonly IList Params; @@ -40,8 +40,12 @@ public AdaptyOnboardingsMultiSelectParams(IList @ public override string ToString() => $"{nameof(Params)}: {Params}"; } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public abstract class AdaptyOnboardingsInput { } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsTextInput : AdaptyOnboardingsInput { public readonly string Value; @@ -52,6 +56,8 @@ public AdaptyOnboardingsTextInput(string value) } } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsEmailInput : AdaptyOnboardingsInput { public readonly string Value; @@ -62,6 +68,8 @@ public AdaptyOnboardingsEmailInput(string value) } } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsNumberInput : AdaptyOnboardingsInput { public readonly double Value; @@ -72,6 +80,8 @@ public AdaptyOnboardingsNumberInput(double value) } } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsInputParams : AdaptyOnboardingsStateUpdatedParams { public readonly AdaptyOnboardingsInput Input; @@ -82,6 +92,8 @@ public AdaptyOnboardingsInputParams(AdaptyOnboardingsInput input) } } + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyOnboardingsDatePickerParams : AdaptyOnboardingsStateUpdatedParams { public readonly int? Day; diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboardingsStateUpdatedParams.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboardingsStateUpdatedParams.cs.meta similarity index 100% rename from Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyOnboardingsStateUpdatedParams.cs.meta rename to Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyOnboardingsStateUpdatedParams.cs.meta diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIOnboardingMeta.cs b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyUIOnboardingMeta.cs similarity index 52% rename from Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIOnboardingMeta.cs rename to Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyUIOnboardingMeta.cs index 8296ec6..d5bc56d 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIOnboardingMeta.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyUIOnboardingMeta.cs @@ -1,32 +1,24 @@ -// -// AdaptyUIOnboardingMeta.cs -// AdaptySDK -// -// Created by GPT-5 on 17.09.2025. -// +using UnityEngine.Scripting; +using System.Runtime.Serialization; namespace AdaptySDK { + [DataContract] + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] public sealed class AdaptyUIOnboardingMeta { + private AdaptyUIOnboardingMeta() { } + + [DataMember(Name = "onboarding_id", IsRequired = true)] public readonly string OnboardingId; + [DataMember(Name = "screen_cid", IsRequired = true)] public readonly string ScreenClientId; + [DataMember(Name = "screen_index", IsRequired = true)] public readonly int ScreenIndex; + [DataMember(Name = "total_screens", IsRequired = true)] public readonly int ScreensTotal; - internal AdaptyUIOnboardingMeta( - string onboardingId, - string screenClientId, - int screenIndex, - int screensTotal - ) - { - OnboardingId = onboardingId; - ScreenClientId = screenClientId; - ScreenIndex = screenIndex; - ScreensTotal = screensTotal; - } - public override string ToString() => $"{nameof(OnboardingId)}: {OnboardingId}, " + $"{nameof(ScreenClientId)}: {ScreenClientId}, " diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIOnboardingMeta.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyUIOnboardingMeta.cs.meta similarity index 100% rename from Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIOnboardingMeta.cs.meta rename to Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyUIOnboardingMeta.cs.meta diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIOnboardingView.cs b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyUIOnboardingView.cs similarity index 81% rename from Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIOnboardingView.cs rename to Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyUIOnboardingView.cs index f38ed53..ee5819a 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIOnboardingView.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyUIOnboardingView.cs @@ -1,16 +1,20 @@ -// -// AdaptyUIOnboardingView.cs -// AdaptySDK -// -// Created by Aleksei Valiano on 17.12.2024. -// +using UnityEngine.Scripting; +using System.Runtime.Serialization; namespace AdaptySDK { - public partial class AdaptyUIOnboardingView + [DataContract] + [Preserve] + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] + public sealed class AdaptyUIOnboardingView { + private AdaptyUIOnboardingView() { } + + [DataMember(Name = "id", IsRequired = true)] public string Id; + [DataMember(Name = "placement_id", IsRequired = true)] public string PlacementId; + [DataMember(Name = "variation_id", IsRequired = true)] public string PaywallVariationId; public override string ToString() => diff --git a/Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIOnboardingView.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyUIOnboardingView.cs.meta similarity index 100% rename from Packages/com.adapty.unity-sdk/Runtime/Models/AdaptyUIOnboardingView.cs.meta rename to Packages/com.adapty.unity-sdk/Runtime/Obsolete/Models/AdaptyUIOnboardingView.cs.meta diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization.meta new file mode 100644 index 0000000..a714691 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 09bfd014c57645e69fd707b0c18f88ea +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters.meta new file mode 100644 index 0000000..1a3a87e --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a535b829dc5b4ffd86c8b579271bef3a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsAnalyticsEvent.cs b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsAnalyticsEvent.cs new file mode 100644 index 0000000..4ec728c --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsAnalyticsEvent.cs @@ -0,0 +1,74 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AdaptySDK.Serialization +{ + /// + /// Onboarding analytics events, chosen by the name discriminator. + /// + /// + /// An event the SDK does not know becomes + /// carrying the raw name, so a newer native SDK can emit events without breaking the listener. + /// + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] + internal sealed class AdaptyConverterOnboardingsAnalyticsEvent : JsonConverter + { + public override bool CanConvert(Type objectType) => + objectType == typeof(AdaptyOnboardingsAnalyticsEvent); + + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => + throw new NotSupportedException(); + + public override object ReadJson( + JsonReader reader, + Type objectType, + object existingValue, + JsonSerializer serializer + ) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + var node = JObject.Load(reader); + var name = AdaptyJsonRequire.String(node, "name"); + + switch (name) + { + case "onboarding_started": + return new AdaptyOnboardingsAnalyticsEventOnboardingStarted(); + + case "screen_presented": + return new AdaptyOnboardingsAnalyticsEventScreenPresented(); + + case "screen_completed": + return new AdaptyOnboardingsAnalyticsEventScreenCompleted( + node.Value("element_id"), + node.Value("reply") + ); + + case "second_screen_presented": + return new AdaptyOnboardingsAnalyticsEventSecondScreenPresented(); + + case "registration_screen_presented": + return new AdaptyOnboardingsAnalyticsEventRegistrationScreenPresented(); + + case "products_screen_presented": + return new AdaptyOnboardingsAnalyticsEventProductsScreenPresented(); + + case "user_email_collected": + return new AdaptyOnboardingsAnalyticsEventUserEmailCollected(); + + case "onboarding_completed": + return new AdaptyOnboardingsAnalyticsEventOnboardingCompleted(); + + default: + return new AdaptyOnboardingsAnalyticsEventUnknown(name); + } + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsAnalyticsEvent.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsAnalyticsEvent.cs.meta new file mode 100644 index 0000000..1749c88 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsAnalyticsEvent.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 241b77baa58c414e9f5a66ceb186cbe9 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsStateUpdatedParams.cs b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsStateUpdatedParams.cs new file mode 100644 index 0000000..a647bdb --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsStateUpdatedParams.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AdaptySDK.Serialization +{ + /// + /// Onboarding state updates: element_type selects the shape, and for an input element a + /// nested type selects the value kind. + /// + /// + /// Unknown element types return null, as the previous parser did — an onboarding built with a + /// newer element must not fail the event. + /// + [System.Obsolete("The legacy onboarding API is deprecated in favor of Flows.")] + internal sealed class AdaptyConverterOnboardingsStateUpdatedParams : JsonConverter + { + public override bool CanConvert(Type objectType) => + objectType == typeof(AdaptyOnboardingsStateUpdatedParams); + + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => + throw new NotSupportedException(); + + public override object ReadJson( + JsonReader reader, + Type objectType, + object existingValue, + JsonSerializer serializer + ) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + var node = JObject.Load(reader); + var elementType = AdaptyJsonRequire.String(node, "element_type"); + + switch (elementType) + { + case "select": + return ReadSelect(AdaptyJsonRequire.Object(node, "value")); + + case "multi_select": + var items = new List(); + foreach (var item in AdaptyJsonRequire.Array(node, "value")) + { + items.Add(ReadSelect(item)); + } + return new AdaptyOnboardingsMultiSelectParams(items); + + case "input": + return ReadInput(AdaptyJsonRequire.Object(node, "value")); + + case "date_picker": + var picker = AdaptyJsonRequire.Object(node, "value"); + return new AdaptyOnboardingsDatePickerParams( + picker.Value("day"), + picker.Value("month"), + picker.Value("year") + ); + + default: + return null; + } + } + + private static AdaptyOnboardingsSelectParams ReadSelect(JToken value) => + new AdaptyOnboardingsSelectParams( + AdaptyJsonRequire.String(value, "id"), + AdaptyJsonRequire.String(value, "value"), + AdaptyJsonRequire.String(value, "label") + ); + + private static AdaptyOnboardingsStateUpdatedParams ReadInput(JToken value) + { + var type = AdaptyJsonRequire.String(value, "type"); + switch (type) + { + case "text": + return new AdaptyOnboardingsInputParams( + new AdaptyOnboardingsTextInput(AdaptyJsonRequire.String(value, "value")) + ); + + case "email": + return new AdaptyOnboardingsInputParams( + new AdaptyOnboardingsEmailInput(AdaptyJsonRequire.String(value, "value")) + ); + + case "number": + return new AdaptyOnboardingsInputParams( + new AdaptyOnboardingsNumberInput(AdaptyJsonRequire.Double(value, "value")) + ); + + default: + return null; + } + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsStateUpdatedParams.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsStateUpdatedParams.cs.meta new file mode 100644 index 0000000..74d64e1 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Obsolete/Serialization/Converters/AdaptyConverterOnboardingsStateUpdatedParams.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fdcca72eeee944ee81b0a35d7cd462b2 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Plugins/AdaptyNoop.cs b/Packages/com.adapty.unity-sdk/Runtime/Plugins/AdaptyNoop.cs index cdfe117..781f742 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Plugins/AdaptyNoop.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Plugins/AdaptyNoop.cs @@ -9,9 +9,26 @@ internal static class AdaptyNoop + (int)AdaptyErrorCode.AdaptyNotInitialized + ",\"message\":\"Adapty SDK is not available in the Unity Editor. Build and run the app on an iOS or Android device to use it.\"}}"; + /// + /// Replaces the canned reply, and sees the request that produced it. + /// + /// + /// The only seam into the transport when there is no native side: the request payload is + /// assembled inside AdaptyRequest.Send, so this is where a test can read what would have + /// gone over the bridge. + /// + internal static Func Handler; + + // Reset for the same reason as the listeners: a hook a previous Play Mode run installed + // must not answer this one. + [UnityEngine.RuntimeInitializeOnLoadMethod( + UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration + )] + internal static void ResetHandler() => Handler = null; + internal static void Invoke(string method, string request, Action completionHandler) { - completionHandler(NotAvailableResponse); + completionHandler(Handler?.Invoke(method, request) ?? NotAvailableResponse); } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/AdaptyAndroid.cs b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/AdaptyAndroid.cs index 1a157af..c1cd12b 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/AdaptyAndroid.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/AdaptyAndroid.cs @@ -1,10 +1,12 @@ -using System; +using System; using UnityEngine; +#if UNITY_ANDROID +using AdaptyAndroidCallback = AdaptySDK.Android.AdaptyAndroidCallbackAction; +#endif + namespace AdaptySDK.Android { #if UNITY_ANDROID - using AdaptyAndroidCallback = AdaptyAndroidCallbackAction; - internal static class AdaptyAndroid { private static AndroidJavaClass AdaptyAndroidClass = new AndroidJavaClass("com.adapty.unity.AdaptyAndroidWrapper"); diff --git a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/AdaptyAndroidCallbackAction.cs b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/AdaptyAndroidCallbackAction.cs index 0baf546..2da89a5 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/AdaptyAndroidCallbackAction.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/AdaptyAndroidCallbackAction.cs @@ -1,4 +1,4 @@ -using System; +using System; using UnityEngine; namespace AdaptySDK.Android @@ -43,10 +43,13 @@ public static AndroidJavaProxy Action(Action action) internal static void InitializeOnce() { - lock(m_Lock) { + lock(m_Lock) { if (!m_IsInitialized) { - m_IsInitialized = true; + // Marked initialized only once registration has returned. Set before the call, + // a throw would leave the flag standing and this method has one caller - a + // [RuntimeInitializeOnLoadMethod] - so nothing would ever try again. new AndroidJavaClass("com.adapty.unity.AdaptyAndroidWrapper").CallStatic("registerMessageHandler", new MessageHandler()); + m_IsInitialized = true; } } } diff --git a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0/unity-wrapper-4.0.0.aar b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0/unity-wrapper-4.0.0.aar deleted file mode 100644 index be11377..0000000 Binary files a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0/unity-wrapper-4.0.0.aar and /dev/null differ diff --git a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0.meta b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1.meta similarity index 100% rename from Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0.meta rename to Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1.meta diff --git a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1/unity-wrapper-4.0.1.aar b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1/unity-wrapper-4.0.1.aar new file mode 100644 index 0000000..fdeb093 Binary files /dev/null and b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1/unity-wrapper-4.0.1.aar differ diff --git a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0/unity-wrapper-4.0.0.aar.meta b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1/unity-wrapper-4.0.1.aar.meta similarity index 100% rename from Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0/unity-wrapper-4.0.0.aar.meta rename to Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1/unity-wrapper-4.0.1.aar.meta diff --git a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0/unity-wrapper-4.0.0.pom b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1/unity-wrapper-4.0.1.pom similarity index 93% rename from Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0/unity-wrapper-4.0.0.pom rename to Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1/unity-wrapper-4.0.1.pom index d9a3fe9..228e2bf 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0/unity-wrapper-4.0.0.pom +++ b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1/unity-wrapper-4.0.1.pom @@ -3,7 +3,7 @@ 4.0.0 io.adapty.internal unity-wrapper - 4.0.0 + 4.0.1 aar diff --git a/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0/unity-wrapper-4.0.0.pom.meta b/Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1/unity-wrapper-4.0.1.pom.meta similarity index 100% rename from Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.0/unity-wrapper-4.0.0.pom.meta rename to Packages/com.adapty.unity-sdk/Runtime/Plugins/Android/Local/io/adapty/internal/unity-wrapper/4.0.1/unity-wrapper-4.0.1.pom.meta diff --git a/Packages/com.adapty.unity-sdk/Runtime/Plugins/iOS/AdaptyIOS.cs b/Packages/com.adapty.unity-sdk/Runtime/Plugins/iOS/AdaptyIOS.cs index 5ac5618..fcf21d0 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Plugins/iOS/AdaptyIOS.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Plugins/iOS/AdaptyIOS.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Runtime.InteropServices; namespace AdaptySDK.iOS diff --git a/Packages/com.adapty.unity-sdk/Runtime/Plugins/iOS/AdaptyIOSCallbackAction.cs b/Packages/com.adapty.unity-sdk/Runtime/Plugins/iOS/AdaptyIOSCallbackAction.cs index 9d01179..d824e83 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/Plugins/iOS/AdaptyIOSCallbackAction.cs +++ b/Packages/com.adapty.unity-sdk/Runtime/Plugins/iOS/AdaptyIOSCallbackAction.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Runtime.InteropServices; using UnityEngine; namespace AdaptySDK.iOS { - internal static class ExceptionGetFullMessage + internal static class AdaptyExceptionExtensions { internal static string GetFullMessage(this Exception ex) { diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization.meta new file mode 100644 index 0000000..dfd27ff --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ce5a3b88719824b9389c0274ea3d4976 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyContractResolver.cs b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyContractResolver.cs new file mode 100644 index 0000000..3437cb5 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyContractResolver.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace AdaptySDK.Serialization +{ + /// + /// Reads the models' System.Runtime.Serialization attributes, with two corrections: + /// IsRequired means present and non-null, not ; and + /// interface-typed collections are contracted as concrete ones, see . + /// + /// + /// Both are contract rules that cannot be stated per member without repeating them across the + /// 128 required members the models declare, or without a backing collection per interface-typed + /// one. Anything a model can say about itself belongs in the model — this is not the place for a + /// convention of the SDK's own. + /// + internal sealed class AdaptyContractResolver : DefaultContractResolver + { + internal static readonly AdaptyContractResolver Instance = new AdaptyContractResolver(); + + protected override JsonContract CreateContract(Type objectType) => base.CreateContract(Concrete(objectType)); + + /// + /// The concrete collection to build for an interface-typed member. Newtonsoft would populate + /// the interface through a CollectionWrapper whose constructor it finds by reflection, + /// and a stripped IL2CPP player no longer has it - which fails every parse. + /// + private static Type Concrete(Type objectType) + { + if (!objectType.IsInterface || !objectType.IsGenericType) + { + return objectType; + } + + var definition = objectType.GetGenericTypeDefinition(); + var arguments = objectType.GetGenericArguments(); + + if (definition == typeof(IList<>) + || definition == typeof(ICollection<>) + || definition == typeof(IEnumerable<>) + || definition == typeof(IReadOnlyList<>) + || definition == typeof(IReadOnlyCollection<>)) + { + return typeof(List<>).MakeGenericType(arguments); + } + + if (definition == typeof(IDictionary<,>) || definition == typeof(IReadOnlyDictionary<,>)) + { + return typeof(Dictionary<,>).MakeGenericType(arguments); + } + + return objectType; + } + + protected override JsonProperty CreateProperty( + MemberInfo member, + MemberSerialization memberSerialization + ) + { + var property = base.CreateProperty(member, memberSerialization); + + if (property.Required == Required.AllowNull) + { + property.Required = Required.Always; + } + + return property; + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyContractResolver.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyContractResolver.cs.meta new file mode 100644 index 0000000..514bf94 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyContractResolver.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d6b1b4cd72d884ef398194baf50e7dfc \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJson.cs b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJson.cs new file mode 100644 index 0000000..f33811a --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJson.cs @@ -0,0 +1,160 @@ +using System; +using System.Globalization; +using Newtonsoft.Json; + +namespace AdaptySDK.Serialization +{ + /// + /// The single entry point to JSON for the SDK. The settings are immutable and the converters + /// hold no state, so serialization can run off the main thread. + /// + internal static class AdaptyJson + { + private static readonly JsonSerializerSettings Settings = CreateSettings(); + + internal static string Serialize(object value) => JsonConvert.SerializeObject(value, Settings); + + internal static T Deserialize(string json) => JsonConvert.DeserializeObject(json, Settings); + + /// + /// Serializes one value into the DOM, for a request assembled key by key. + /// + internal static Newtonsoft.Json.Linq.JToken ToNode(object value) => + value is null + ? Newtonsoft.Json.Linq.JValue.CreateNull() + : Newtonsoft.Json.Linq.JToken.FromObject(value, CreateSerializer()); + + /// + /// Serializes a request and stamps the method name into it, as a sibling of the parameters + /// rather than a wrapper around them. + /// + internal static string SerializeRequest(string method, object request) + { + var node = + request is null ? new Newtonsoft.Json.Linq.JObject() + : request is Newtonsoft.Json.Linq.JObject given ? (Newtonsoft.Json.Linq.JObject)given.DeepClone() + : Newtonsoft.Json.Linq.JObject.FromObject(request, CreateSerializer()); + + node["method"] = method; + return node.ToString(Formatting.None); + } + + /// + /// Builds the DOM for a payload arriving from native code. + /// + /// + /// Not JToken.Parse. Its reader defaults to DateParseHandling.DateTime with + /// RoundtripKind, so every ISO string becomes a while the tree + /// is being built - before a converter or a setting has any say. Typed dates would then + /// reach the app as UTC rather than local, and a date-looking string in an untyped payload + /// would come back reformatted instead of as it was sent. + /// + internal static Newtonsoft.Json.Linq.JToken ParseDocument(string json) + { + using (var reader = new JsonTextReader(new System.IO.StringReader(json)) + { + DateParseHandling = DateParseHandling.None, + FloatParseHandling = FloatParseHandling.Double, + }) + { + var document = Newtonsoft.Json.Linq.JToken.Load(reader); + + // What JToken.Parse does after loading, and the reason a truncated payload cannot + // pass for a whole one. Read to the end rather than once: a comment between two + // documents would otherwise hide the second, and "{}/* c */{...}" would be taken + // for "{}". + while (reader.Read()) + { + if (reader.TokenType != JsonToken.Comment) + { + throw new JsonReaderException( + "Additional text found after the JSON document." + ); + } + } + + return document; + } + } + + /// + /// Reads a remote config's data, which is JSON the contract does not describe. + /// + internal static System.Collections.Generic.IDictionary + DeserializeRemoteConfigDictionary(string json) + { + var type = typeof(System.Collections.Generic.IDictionary); + + using (var reader = new JsonTextReader(new System.IO.StringReader(json))) + { + return (System.Collections.Generic.IDictionary) + CreateSerializerFor(type).Deserialize(reader, type); + } + } + + /// + /// A serializer for reading one value of a known type, carrying the loose converter when + /// that type is one the contract leaves untyped. + /// + /// + /// The loose converter is deliberately absent from the shared settings, so an ordinary + /// Dictionary<string, object> keeps Newtonsoft's own shapes. What must not + /// happen is a public payload the contract types as a bare object losing the CLR graph it + /// gave in 3.x - so the decision is made here, once, from the type being asked for, rather + /// than restated at each call site. + /// + internal static JsonSerializer CreateSerializerFor(Type type) + { + var serializer = CreateSerializer(); + + if (AdaptyConverterLooseJson.Instance.CanConvert(type)) + { + serializer.Converters.Add(AdaptyConverterLooseJson.Instance); + } + + return serializer; + } + + /// + /// A serializer for call sites that read a sub-token. Created per call, since + /// is not documented as thread-safe; the settings and the + /// resolver's contract cache behind it are shared, so it stays cheap. + /// + internal static JsonSerializer CreateSerializer() => JsonSerializer.Create(Settings); + + private static JsonSerializerSettings CreateSettings() => + new JsonSerializerSettings + { + // The manual layer could not emit an explicit null, so neither does this one. + NullValueHandling = NullValueHandling.Ignore, + MissingMemberHandling = MissingMemberHandling.Ignore, + + // Keeps date-looking strings in payload_data and custom attributes round-tripping + // as written instead of becoming DateTime. + DateParseHandling = DateParseHandling.None, + FloatParseHandling = FloatParseHandling.Double, + + Culture = CultureInfo.InvariantCulture, + TypeNameHandling = TypeNameHandling.None, + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + + // Models are built from native responses only, through a private constructor. + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, + + // Omitted collections keep their empty field initializer, so callers iterate without + // null checks; Replace, so a present one is not appended to it. + ObjectCreationHandling = ObjectCreationHandling.Replace, + + ContractResolver = AdaptyContractResolver.Instance, + Converters = new JsonConverter[] + { + new AdaptyConverterDateTime(), + new AdaptyConverterStringEnum(), + new AdaptyConverterOnboardingsStateUpdatedParams(), + new AdaptyConverterOnboardingsAnalyticsEvent(), + new AdaptyConverterSubscriptionOffer(), + new AdaptyConverterCustomAssets(), + }, + }; + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJson.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJson.cs.meta new file mode 100644 index 0000000..e402c4b --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJson.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: eef7d592453a5431b939dd457155b321 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJsonRequire.cs b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJsonRequire.cs new file mode 100644 index 0000000..0380053 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJsonRequire.cs @@ -0,0 +1,75 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AdaptySDK.Serialization +{ + /// + /// Required-value accessors for the hand-written converters. + /// + /// + /// A converter reads its payload itself, so it never goes through + /// and gets no Required.Always checking. Plain + /// JToken.Value<T>(key) would turn a missing contract-required key into null or 0 + /// and hand the listener a half-built model; these throw the way the SimpleJSON layer did. + /// + /// This is separate from the unknown-discriminator fallback: a value the SDK does not know yet + /// is forward compatibility, a missing required value is a malformed payload. + /// + internal static class AdaptyJsonRequire + { + internal static JObject Object(JToken node, string key) + { + if (node?[key] is not JObject value) + { + throw Missing(key); + } + return value; + } + + internal static JArray Array(JToken node, string key) + { + if (node?[key] is not JArray value) + { + throw Missing(key); + } + return value; + } + + internal static JToken Token(JToken node, string key) + { + var value = node?[key]; + if (value is null || value.Type == JTokenType.Null) + { + throw Missing(key); + } + return value; + } + + internal static string String(JToken node, string key) + { + var value = node?[key]; + if (value is null || value.Type == JTokenType.Null) + { + throw Missing(key); + } + return value.Value(); + } + + internal static double Double(JToken node, string key) + { + var value = node?[key]; + if (value is null || value.Type == JTokenType.Null) + { + throw Missing(key); + } + return value.Value(); + } + + /// + /// The same failure for a key a model can only require conditionally, on the branch its + /// discriminator selects. + /// + internal static JsonSerializationException Missing(string key) => + new JsonSerializationException($"Required property '{key}' not found in JSON."); + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJsonRequire.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJsonRequire.cs.meta new file mode 100644 index 0000000..93ebb75 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyJsonRequire.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2e40c89a16ca642e6847c612644d4196 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyPaywallProductRequest.cs b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyPaywallProductRequest.cs new file mode 100644 index 0000000..9a8556c --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyPaywallProductRequest.cs @@ -0,0 +1,101 @@ +using System.Runtime.Serialization; +using UnityEngine.Scripting; + +namespace AdaptySDK.Serialization +{ + /// + /// What the native side expects when a product is handed back to it - a strict subset of what + /// it sent, with the subscription offer flattened to an identifier. + /// + /// + /// The model itself cannot express this: a member carries one contract name for both + /// directions, so serializing as-is would return the whole + /// product, price and localization included. + /// + [DataContract] + [Preserve] + internal sealed class AdaptyPaywallProductRequest + { + internal AdaptyPaywallProductRequest(AdaptyPaywallProduct product) + { + VendorProductId = product.VendorProductId; + AdaptyProductId = product.AdaptyProductId; + AccessLevelId = product.AccessLevelId; + ProductType = product.ProductType; + PaywallVariationId = product.PaywallVariationId; + PaywallABTestName = product.PaywallABTestName; + PaywallName = product.PaywallName; + PaywallProductIndex = product.PaywallProductIndex; + WebPurchaseUrl = product.WebPurchaseUrl; + PayloadData = product.PayloadData; + + var offer = product.Subscription?.Offer; + if (offer != null) + { + Offer = new OfferIdentifier(offer.Identifier, offer.Type); + } + } + + [DataMember(Name = "vendor_product_id", IsRequired = true)] + [Preserve] + private string VendorProductId { get; } + + [DataMember(Name = "adapty_product_id", IsRequired = true)] + [Preserve] + private string AdaptyProductId { get; } + + [DataMember(Name = "access_level_id", IsRequired = true)] + [Preserve] + private string AccessLevelId { get; } + + [DataMember(Name = "product_type", IsRequired = true)] + [Preserve] + private string ProductType { get; } + + [DataMember(Name = "paywall_variation_id", IsRequired = true)] + [Preserve] + private string PaywallVariationId { get; } + + [DataMember(Name = "paywall_ab_test_name", IsRequired = true)] + [Preserve] + private string PaywallABTestName { get; } + + [DataMember(Name = "paywall_name", IsRequired = true)] + [Preserve] + private string PaywallName { get; } + + [DataMember(Name = "paywall_product_index", IsRequired = true)] + [Preserve] + private int PaywallProductIndex { get; } + + [DataMember(Name = "web_purchase_url")] + [Preserve] + private string WebPurchaseUrl { get; } + + [DataMember(Name = "payload_data")] + [Preserve] + private string PayloadData { get; } + + [DataMember(Name = "subscription_offer_identifier")] + [Preserve] + private OfferIdentifier Offer { get; } + + [DataContract] + private sealed class OfferIdentifier + { + internal OfferIdentifier(string id, AdaptySubscriptionOfferType type) + { + Id = id; + Type = type; + } + + [DataMember(Name = "id")] + [Preserve] + private string Id { get; } + + [DataMember(Name = "type", IsRequired = true)] + [Preserve] + private AdaptySubscriptionOfferType Type { get; } + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyPaywallProductRequest.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyPaywallProductRequest.cs.meta new file mode 100644 index 0000000..861f82b --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyPaywallProductRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9cbf68574248243ec9afc924050f0e1e \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyResponse.cs b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyResponse.cs new file mode 100644 index 0000000..1a43c2f --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyResponse.cs @@ -0,0 +1,57 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AdaptySDK.Serialization +{ + /// + /// The envelope every native reply arrives in: either error or success. + /// + /// + /// Nothing here is allowed to throw. The reply is parsed on a callback from native code, which + /// on IL2CPP is a reverse-P/Invoke boundary with no handler behind it, so a malformed payload + /// has to come back as an rather than as an exception. + /// + internal static class AdaptyResponse + { + internal static AdaptyResult Parse(string json) + { + try + { + if (AdaptyJson.ParseDocument(json) is not JObject response) + { + throw new JsonSerializationException("The reply is not an object."); + } + + var error = response["error"]; + + if (error != null && error.Type != JTokenType.Null) + { + return new AdaptyResult( + default(T), + error.ToObject(AdaptyJson.CreateSerializer()) + ); + } + + // Required, not optional. A reply carrying neither member is malformed, and + // silently reporting it as a successful default would turn a broken bridge into a + // false negative - "not premium", "purchase did not happen". + return new AdaptyResult( + AdaptyJsonRequire.Token(response, "success").ToObject(AdaptyJson.CreateSerializer()), + null + ); + } + catch (Exception ex) + { + return new AdaptyResult( + default(T), + new AdaptyError( + AdaptyErrorCode.DecodingFailed, + "Failed decoding result ", + $"AdaptyUnityError.DecodingFailed({ex})" + ) + ); + } + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyResponse.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyResponse.cs.meta new file mode 100644 index 0000000..5efb131 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/AdaptyResponse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 79df4395b70024969a2b642787b9ee7c \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters.meta new file mode 100644 index 0000000..242f6b1 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d67c5a53abd4451cb806acbddf653b47 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterCustomAssets.cs b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterCustomAssets.cs new file mode 100644 index 0000000..4ddeaf3 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterCustomAssets.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AdaptySDK.Serialization +{ + /// + /// Custom assets travel as an array, not as a map: the key the app used becomes the element's + /// id. + /// + internal sealed class AdaptyConverterCustomAssets : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(Dictionary); + + public override bool CanRead => false; + + public override object ReadJson( + JsonReader reader, + Type objectType, + object existingValue, + JsonSerializer serializer + ) => throw new NotSupportedException(); + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + return; + } + + var assets = (Dictionary)value; + + writer.WriteStartArray(); + foreach (var entry in assets) + { + var node = JObject.FromObject(entry.Value, serializer); + node["id"] = entry.Key; + node.WriteTo(writer); + } + writer.WriteEndArray(); + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterCustomAssets.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterCustomAssets.cs.meta new file mode 100644 index 0000000..757fb36 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterCustomAssets.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bf58da6976504af9b645f41a0e443949 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterDateTime.cs b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterDateTime.cs new file mode 100644 index 0000000..1de3538 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterDateTime.cs @@ -0,0 +1,107 @@ +using System; +using System.Globalization; +using Newtonsoft.Json; + +namespace AdaptySDK.Serialization +{ + /// + /// Translates between the two time zones the SDK speaks: the wire is UTC, the public API is + /// local. + /// + /// + /// This is a decision, not inherited behaviour, and the two directions are mirrors of each + /// other: + /// + /// reading turns the contract's UTC string into the same instant as local time, because + /// the dates the SDK hands back - a subscription's expiry, an access level's activation - are + /// shown to end users, and expiresAt > DateTime.Now is what an app naturally + /// writes; + /// writing takes it back, and reads a DateTimeKind.Unspecified value as local for + /// the same reason: an app that builds a countdown from new DateTime(2026, 7, 30, 22, 0, 0) + /// means 22:00 on the user's clock. DateTime.ToUniversalTime resolves it the same + /// way. + /// + /// Neither half can move to DateTimeZoneHandling. Set to Utc it reads correctly + /// but relabels an unspecified value on write instead of converting it, which would shift every + /// custom timer by the user's offset. + /// + /// What the convention leaves to the app: a local time inside a daylight saving transition is + /// ambiguous, and a device that changes time zone shows different digits for the same + /// subscription. Call where an instant is what matters. + /// + /// + internal sealed class AdaptyConverterDateTime : JsonConverter + { + private const string Format = "yyyy-MM-ddTHH:mm:ss.fffZ"; + + public override bool CanConvert(Type objectType) => objectType == typeof(DateTime) || objectType == typeof(DateTime?); + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + return; + } + + var moment = (DateTime)value; + if (moment.Kind != DateTimeKind.Utc) + { + moment = moment.ToUniversalTime(); + } + + writer.WriteValue(moment.ToString(Format, CultureInfo.InvariantCulture)); + } + + public override object ReadJson( + JsonReader reader, + Type objectType, + object existingValue, + JsonSerializer serializer + ) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + // A reader that recognised the date itself. AdaptyJson.ParseDocument keeps that from + // happening on the SDK's own paths, and this is the belt: the contract decides the + // kind, not whichever reader got there first. An unspecified one came from a string the + // contract writes as UTC. + if (reader.Value is DateTime alreadyParsed) + { + return alreadyParsed.Kind == DateTimeKind.Local + ? alreadyParsed + : DateTime.SpecifyKind(alreadyParsed, DateTimeKind.Utc).ToLocalTime(); + } + + var text = reader.Value as string; + if (text is null) + { + throw new JsonSerializationException( + $"Expected a date string, got {reader.TokenType}" + ); + } + + try + { + // AssumeUniversal is what makes this branch agree with the one above about a string + // carrying no designator: the wire is UTC, so a value with nothing to say otherwise + // is UTC. Left to the default styles it would come back Unspecified and unconverted, + // which moves the instant by the device's offset. A string that does carry Z or an + // offset is unaffected - AdjustToUniversal resolves it, and ToLocalTime puts both + // shapes back on the contract's local side. + return DateTime.Parse( + text, + CultureInfo.InvariantCulture, + DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal + ).ToLocalTime(); + } + catch (Exception e) + { + throw new JsonSerializationException($"Failed decoding DateTime from \"{text}\"", e); + } + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterDateTime.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterDateTime.cs.meta new file mode 100644 index 0000000..ee03837 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterDateTime.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e7da115dc5c4b47269e9e0901a7ad8e2 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterLooseJson.cs b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterLooseJson.cs new file mode 100644 index 0000000..fc45e88 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterLooseJson.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AdaptySDK.Serialization +{ + /// + /// Reads the JSON the contract does not type into plain + /// and values, with every number as + /// . + /// + /// + /// Newtonsoft would hand back JObject / JArray and long, and the three + /// payloads this serves are public API that gave a CLR graph of doubles in 3.x. Each reaches it + /// a different way, because each arrives a different way: + /// + /// AdaptyProfile.CustomAttributes is a member and names this converter in a + /// [JsonConverter] of its own - which is why the type needs [Preserve], since + /// Newtonsoft then constructs it by reflection; + /// AdaptyRemoteConfig.Dictionary is a string parsed on demand, through + /// AdaptyJson.DeserializeRemoteConfigDictionary; + /// the analytic event's params is a sub-token the dispatcher reads, through + /// AdaptyJson.CreateSerializerFor, which covers every Required and + /// Optional rather than that one event. + /// + /// It is not in the shared settings, so any other bare object keeps Newtonsoft's own + /// shapes. All three ask what counts as loose - it is the only place + /// the type list is written down. Verified to behave the same way on IL2CPP. + /// + [UnityEngine.Scripting.Preserve] + internal sealed class AdaptyConverterLooseJson : JsonConverter + { + /// + /// Shared because the three routes to it all have to agree on what "loose" means, and the + /// converter holds no state. + /// + internal static readonly AdaptyConverterLooseJson Instance = new AdaptyConverterLooseJson(); + + public override bool CanConvert(Type objectType) => + objectType == typeof(object) + || objectType == typeof(IDictionary) + || objectType == typeof(Dictionary) + || objectType == typeof(IList) + || objectType == typeof(List); + + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => + throw new NotSupportedException(); + + public override object ReadJson( + JsonReader reader, + Type objectType, + object existingValue, + JsonSerializer serializer + ) => Convert(JToken.Load(reader)); + + private static object Convert(JToken token) + { + switch (token.Type) + { + case JTokenType.Object: + var map = new Dictionary(); + foreach (var property in (JObject)token) + { + map[property.Key] = Convert(property.Value); + } + return map; + + case JTokenType.Array: + var list = new List(); + foreach (var item in (JArray)token) + { + list.Add(Convert(item)); + } + return list; + + case JTokenType.Integer: + case JTokenType.Float: + return token.Value(); + + case JTokenType.Boolean: + return token.Value(); + + case JTokenType.Null: + case JTokenType.Undefined: + return null; + + default: + return token.Value(); + } + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterLooseJson.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterLooseJson.cs.meta new file mode 100644 index 0000000..74dd595 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterLooseJson.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cda5bbd1178344fa6932c42883c6fc9e \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterStringEnum.cs b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterStringEnum.cs new file mode 100644 index 0000000..3b1723b --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterStringEnum.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace AdaptySDK.Serialization +{ + /// + /// Writes enums with stock and reads them back exactly: + /// a value is one of the contract's own strings or it is not a value at all. + /// + /// + /// Two departures from stock, and both are load-bearing. + /// claims every enum, while the ones without + /// names are numeric in the contract - + /// carries the native error code and + /// the ATT raw value - so + /// leaves those to the default numeric handling. And stock reading is + /// lenient in three ways the contract does not allow: it matches the C# member name as well as + /// the one, ignores case, and trims the value. Reading is + /// therefore an ordinal lookup here rather than a call to the base. + /// + internal sealed class AdaptyConverterStringEnum : StringEnumConverter + { + private static readonly ConcurrentDictionary> Cache = + new ConcurrentDictionary>(); + + internal AdaptyConverterStringEnum() + { + AllowIntegerValues = false; + } + + public override bool CanConvert(Type objectType) + { + var type = Nullable.GetUnderlyingType(objectType) ?? objectType; + + return type.IsEnum && ContractNames(type).Count > 0; + } + + public override object ReadJson( + JsonReader reader, + Type objectType, + object existingValue, + JsonSerializer serializer + ) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + var type = Nullable.GetUnderlyingType(objectType) ?? objectType; + + if (reader.TokenType == JsonToken.String + && ContractNames(type).TryGetValue((string)reader.Value, out var known)) + { + return known; + } + + throw new JsonSerializationException( + $"{type.Name} unknown value: {reader.Value ?? "null"}" + ); + } + + private static Dictionary ContractNames(Type type) => + Cache.GetOrAdd(type, Build); + + private static Dictionary Build(Type type) + { + var names = new Dictionary(StringComparer.Ordinal); + + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static)) + { + var attribute = field.GetCustomAttribute(); + if (attribute != null) + { + names[attribute.Value] = field.GetValue(null); + } + } + + return names; + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterStringEnum.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterStringEnum.cs.meta new file mode 100644 index 0000000..e0fbf9b --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterStringEnum.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 83c1ea516a0d413bbbbb0ef40dbd9b8a \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterSubscriptionOffer.cs b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterSubscriptionOffer.cs new file mode 100644 index 0000000..3f5cda1 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterSubscriptionOffer.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AdaptySDK.Serialization +{ + /// + /// Flattens the contract's nested offer_identifier onto the model's Identifier/Type + /// pair. + /// + /// + /// Kept out of the model so it needs no Newtonsoft attribute: the models carry + /// System.Runtime.Serialization annotations only, which is what makes a later move to + /// another serializer a matter of replacing converters. + /// + internal sealed class AdaptyConverterSubscriptionOffer : JsonConverter + { + public override bool CanConvert(Type objectType) => + objectType == typeof(AdaptySubscriptionOffer); + + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => + throw new NotSupportedException(); + + public override object ReadJson( + JsonReader reader, + Type objectType, + object existingValue, + JsonSerializer serializer + ) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + var node = JObject.Load(reader); + var identity = AdaptyJsonRequire.Object(node, "offer_identifier"); + + // The key is not even looked at off Android: an Android-only value of an unexpected + // shape must not be able to fail a read that never uses it. +#if UNITY_ANDROID + var offerTags = node["offer_tags"]?.ToObject>(serializer); +#else + IList offerTags = null; +#endif + + // Read before the id: the contract requires the id in two of the type's branches only, + // so which rule applies is not known until the type is. + var type = AdaptyJsonRequire + .Token(identity, "type") + .ToObject(serializer); + + var idIsRequired = + type == AdaptySubscriptionOfferType.Promotional + || type == AdaptySubscriptionOfferType.WinBack; +#if UNITY_ANDROID + // The contract marks the id required on the introductory branch for Android as well. + idIsRequired = idIsRequired || type == AdaptySubscriptionOfferType.Introductory; +#endif + + var identifier = idIsRequired + ? AdaptyJsonRequire.String(identity, "id") + : identity.Value("id"); + + return new AdaptySubscriptionOffer( + identifier, + type, + AdaptyJsonRequire + .Array(node, "phases") + .ToObject>(serializer), + offerTags + ); + } + } +} diff --git a/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterSubscriptionOffer.cs.meta b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterSubscriptionOffer.cs.meta new file mode 100644 index 0000000..6eb48e1 --- /dev/null +++ b/Packages/com.adapty.unity-sdk/Runtime/Serialization/Converters/AdaptyConverterSubscriptionOffer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cd76ca1571a224facbc9bac37de03276 \ No newline at end of file diff --git a/Packages/com.adapty.unity-sdk/Runtime/com.adapty.unity-sdk.asmdef b/Packages/com.adapty.unity-sdk/Runtime/com.adapty.unity-sdk.asmdef index 4e1ba75..f272e0d 100644 --- a/Packages/com.adapty.unity-sdk/Runtime/com.adapty.unity-sdk.asmdef +++ b/Packages/com.adapty.unity-sdk/Runtime/com.adapty.unity-sdk.asmdef @@ -8,7 +8,15 @@ "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, - "defineConstraints": [], - "versionDefines": [], + "defineConstraints": [ + "ADAPTY_NEWTONSOFT" + ], + "versionDefines": [ + { + "name": "com.unity.nuget.newtonsoft-json", + "expression": "", + "define": "ADAPTY_NEWTONSOFT" + } + ], "noEngineReferences": false } diff --git a/Packages/com.adapty.unity-sdk/package.json b/Packages/com.adapty.unity-sdk/package.json index 1afa9d8..789dc0e 100644 --- a/Packages/com.adapty.unity-sdk/package.json +++ b/Packages/com.adapty.unity-sdk/package.json @@ -1,10 +1,14 @@ { "name": "com.adapty.unity-sdk", - "version": "4.0.0", + "version": "4.0.0-beta.2", "displayName": "Adapty Unity SDK", "description": "Unity SDK for Adapty in-app purchases, subscriptions, paywalls, and onboarding flows.", + "unity": "2022.3", "_upm": { - "changelog": "### Added\n\n- **iOS Kids Mode** for the App Store Kids Category / COPPA: the `ADAPTY_KIDS_MODE` scripting define enables the `KidsMode` trait on the AdaptySDK-iOS Swift package (IDFA, AdSupport and AppTrackingTransparency are compiled out of the binary) and forces `apple_idfa_collection_disabled` in the runtime configuration. Requires Xcode 26 or newer. Set the define in Player Settings; a build profile's scripting defines work too, but only once Unity has recompiled the Editor assemblies for them, and the iOS build fails if the SDK detects that it is running against stale ones. `BuildPlayerOptions.extraScriptingDefines` is not supported at all.\n- `AdaptyUICreateFlowViewParameters.Locale` — the localization to render the flow with. A flow is localized when its view is built, so this is the only place that selects the localization. Requires the native iOS 4.0.2 and Android 4.0.1 releases, both of which are now the pinned dependencies.\n- `AdaptyUIFlowView.Locale` — the localization the view was actually built with.\n- `AdaptyUICreateFlowViewParameters.EnableSafeAreaPaddings` (Android only).\n\n### Changed\n\n- **Breaking:** migrated the paywall API to flows — `AdaptyPaywall` → `AdaptyFlow`, `GetPaywall` → `GetFlow`, `CreatePaywallView` → `CreateFlowView`, with the corresponding models, events and view controllers. Event listener interfaces now carry the `I` prefix.\n- The legacy onboarding API is deprecated in favor of flows.\n- Updated the cross-platform contract to 4.0.2 and the native SDK dependencies to iOS 4.0.2 and Android 4.0.1.\n\n### Fixed\n\n- `ReportTransaction` no longer reports `DecodingFailed` for transactions that were in fact reported successfully.\n- Custom linear gradient assets are now serialized from every color and alpha key of the Unity `Gradient`.\n- `AdaptyProductIdentifier` now implements value equality, so identifiers built from a flow work as keys in the dictionary passed to `AdaptyUICreateFlowViewParameters.SetProductPurchaseParameters`.\n- `AdaptyFlow.VendorProductIds` and `AdaptyFlow.ProductIdentifiers` no longer return duplicates across paywall variations.\n- Calling the SDK in the Editor now returns a readable \"not supported on this platform\" error." + "changelog": "Upgrading from 3.x: see [MIGRATION-v3.17-to-v4.0.md](https://github.com/adaptyteam/AdaptySDK-Unity/blob/4.0.0-beta.2/MIGRATION-v3.17-to-v4.0.md). If you install from a `.unitypackage`, delete `Assets/AdaptySDK` and add `com.unity.nuget.newtonsoft-json` **before** importing: a `.unitypackage` never removes files, so the 62 sources this release drops would otherwise stay behind and collide with the new ones. Until Newtonsoft is there the SDK assembly is skipped, so code that calls Adapty will not compile — and once it does not, Unity stops loading the Editor assembly the installer menu lives in.\n\n### Added\n\n- **iOS Kids Mode** for the App Store Kids Category / COPPA: the `ADAPTY_KIDS_MODE` scripting define enables the `KidsMode` trait on the AdaptySDK-iOS Swift package (IDFA, AdSupport and AppTrackingTransparency are compiled out of the binary) and forces `apple_idfa_collection_disabled` in the runtime configuration. Swift package traits need Xcode 26, which v4 requires anyway. Set the define in Player Settings; a build profile's scripting defines work too, but only once Unity has recompiled the Editor assemblies for them, and the iOS build fails if the SDK detects that it is running against stale ones. `BuildPlayerOptions.extraScriptingDefines` is not supported at all.\n- `AdaptyUICreateFlowViewParameters.Locale` — the localization to render the flow with. A flow is localized when its view is built, so this is the only place that selects the localization. Requires the native iOS 4.0.2 and Android 4.0.1 releases, both of which are now the pinned dependencies.\n- `AdaptyUIFlowView.Locale` — the localization the view was actually built with.\n- `AdaptyUICreateFlowViewParameters.EnableSafeAreaPaddings` (Android only).\n- **Adapty SDK > Install Dependencies** — installs whichever of the SDK's package dependencies are missing: Newtonsoft.Json, and External Dependency Manager along with the OpenUPM scoped registry it is published on. A `.unitypackage` carries assets only and can bring neither, and External Dependency Manager has always had to be installed by hand even alongside Package Manager. Packages already in the project are left as they are, apart from an External Dependency Manager older than the SDK needs, which is upgraded.\n- `AdaptyErrorCode.NoPurchasesToRestore` (1004) — restored. The member was commented out in December 2024 while the native Android SDK kept sending the code, so `RestorePurchases` on a profile with nothing to restore returned an error that could only be matched against a literal `1004`. Nothing about the error changes; it now has its name back. The code is Android-only — iOS does not define it.\n- Seven more `AdaptyErrorCode` members the native SDKs declare but this enum never named: `UnidentifiedUserLogout` (3020, both platforms, from `Logout` on an unidentified profile), `PaymentPendingError` (1050, iOS), `BillingNetworkError` (112, Android), and `WrongAssetType` (4104), `JsException` (4105), `NavigatorNotFound` (4106), `InvalidActionUrl` (4107) — the four the Android flow renderer reports through `FlowViewDidReceiveError`. `AdaptyErrorCode` carries the native number, so these codes always arrived; they simply had no constant to match against. Each one was traced in the iOS 4.0.2 and Android 4.0.1 sources to the place the native SDK produces it — a throw site for all but 112, which comes out of the `fromBilling` mapping. `PaymentPendingError` is the one exception to \"already arrived\", and is named for completeness rather than to be handled: its only throw site is an iOS overload taking StoreKit's own purchase result, which the Unity bridge never calls. A pending purchase made through the SDK arrives as `AdaptyPurchaseResultType.Pending`.\n\n### Changed\n\n- **The minimum supported Unity version is now declared: 2022.3.** It was never stated before, in `package.json` or anywhere else, so Package Manager let any Editor install a package it might not be able to compile. Nothing was dropped — the floor is now stated. Installing on the floor is verified: a clean `.unitypackage` import and **Adapty SDK > Install Dependencies** were run end to end on 2022.3, and the SDK compiles afterwards. Player builds, device runs and the rest of the acceptance matrix were done on Unity 6, which is what the SDK is developed against.\n- **The JSON layer now uses Newtonsoft.Json instead of the bundled SimpleJSON.** The package depends on `com.unity.nuget.newtonsoft-json` 3.2.2, which Package Manager installs for you and **Adapty SDK > Install Dependencies** installs for everyone else. While Newtonsoft is absent the SDK assembly is skipped by a define constraint instead of failing to compile. The SDK reports the reason in the Editor console — as long as its Editor assembly loads, which it does not while your own scripts fail to compile. A second copy of Newtonsoft is reported the same way, since it makes its types ambiguous. The models, their members and every method signature are unchanged, so calling code is unaffected. **Breaking for anything that used `AdaptySDK.SimpleJSON` directly:** the namespace is gone, and its public types (`JSON`, `JSONNode`, `JSONObject`, `JSONArray` and the rest) went with it.\n- **Breaking:** migrated the paywall API to flows — `AdaptyPaywall` → `AdaptyFlow`, `GetPaywall` → `GetFlow`, `CreatePaywallView` → `CreateFlowView`, with the corresponding models, events and view controllers. Event listener interfaces now carry the `I` prefix.\n- The legacy onboarding API is deprecated in favor of flows, and `[Obsolete]` now covers the whole of it rather than only its entry points: `IAdaptyOnboardingsEventsListener`, `AdaptyOnboarding`, `AdaptyUIOnboardingView`, `AdaptyUIOnboardingMeta`, the `AdaptyOnboardingsAnalyticsEvent`, `AdaptyOnboardingsStateUpdatedParams` and `AdaptyOnboardingsInput` hierarchies, and the `AdaptyUI.ShowDialog` overload taking an `AdaptyUIOnboardingView`.\n- **Breaking:** `Adapty.GetLoglevel` is spelled `Adapty.GetLogLevel`. The typo was in the v3 surface too, out of step with its own `SetLogLevel` and with `get_log_level`, the operation the cross-platform contract names. Same signature, same wire method.\n- **Breaking:** removed two members that only forwarded to another one — `AdaptyProfile.NonSubscription.IsOneTime` (returned `IsConsumable` unchanged) and `AdaptyPlacement.GetIsTrackingPurchases` (wrapped the public `IsTrackingPurchases` field, whose `null` case cannot occur).\n- **Breaking:** a string the contract does not list now fails the read instead of degrading to `Unknown`, and the six members that existed only to catch one are gone: `AdaptyPurchaseResultType.Unknown`, `AdaptySubscriptionOfferType.Unknown`, `AdaptySubscriptionRenewalType.Unknown`, `AdaptyUIDialogActionType.Unknown`, `AdaptyUIUserActionType.Unknown` and `AdaptyWebPresentation.Unknown`. The SDK ships pinned to the native SDKs it is built against, so an unlisted value is a broken payload rather than one from the future. `AdaptyPaymentMode.Unknown` and `AdaptySubscriptionPeriodUnit.Unknown` stay, because the contract lists `\"unknown\"` among their values. No surviving member changed its numeric value, and a JSON number is no longer accepted for a string enum.\n- **iOS builds now require Xcode 26 or newer.** AdaptySDK-iOS 4.0 declares `swift-tools-version: 6.2`, where the 3.17.2 that v3 pinned declared 6.0, and Swift Package Manager refuses a package whose tools version is newer than the installed toolchain. On Xcode 16 the build fails while resolving the dependency, before anything is compiled. This is the floor for the whole SDK, not only for Kids Mode. Nothing in Unity can check it — the Editor never sees which Xcode will open the generated project.\n- Updated the cross-platform contract to 4.0.2 and the native SDK dependencies to iOS 4.0.2 and Android 4.0.1.\n- Errors returned by `flow_view_did_answer_permission` and by the observer-mode round trips are now logged instead of being swallowed.\n- **Breaking:** removed `AdaptyErrorCode.InvalidJson` (23) and `AdaptyErrorCode.PendingPurchase` (25). Neither native SDK declares these codes any more, so nothing can raise them; a pending purchase is reported as `AdaptyPurchaseResultType.Pending`.\n- **Breaking:** removed what the old JSON layer left behind — `AdaptyRefundPreferenceExtensions.ToJSONNode`, the last of the `ToJSONNode` extension classes, which the SDK does not call because the refund preference is serialized through its `[EnumMember]` mapping, and two constructors that only the hand-written parser ever called.\n- **Breaking:** the collections on a response model are read-only. `AdaptyProfile`, `AdaptyFlow`, `AdaptyFlowPaywall`, `AdaptySubscriptionOffer` and `AdaptyRemoteConfig` hand back `IReadOnlyList` and `IReadOnlyDictionary`, and `AdaptyProfile.NonSubscriptions` is read-only at both levels. A `readonly` field never made these models immutable: the reference could not be replaced, but the contents could. `GetPaywallProducts` reports an `IReadOnlyList` for the same reason. The deprecated onboarding API is the exception and keeps its old shapes — it is maintained rather than improved until it is removed.\n- **Breaking:** the parameter objects take the narrowest abstraction and copy it. `AdaptyUICreateFlowViewParameters.SetCustomTags`, `SetCustomTimers`, `SetCustomAssets` and `SetProductPurchaseParameters` accept an `IReadOnlyDictionary` and copy it; the four matching members are read-only properties rather than public fields. `UpdateAttribution` takes an `IReadOnlyDictionary`, and `AdaptyProfileParameters.CustomAttributes` exposes a view.\n- **Breaking, at the call site only:** the analytics-event parameter of `IAdaptyFlowsEventsListener.FlowViewDidReceiveAnalyticEvent` is named `parameters` rather than `@params`. The CLR signature is unchanged, so only a call passing it as a named argument has to be renamed.\n- **Breaking:** `IAdaptyFlowsEventsListener.FlowViewDidReceiveAnalyticEvent` and `IAdaptyUISystemRequestsHandler.FlowViewDidAskPermission` receive `IReadOnlyDictionary` instead of `IDictionary`. Implementations need the signature updated.\n- **Breaking:** `AdaptyPlacementFetchPolicy.Default`, `.ReloadRevalidatingCacheData` and `.ReturnCacheDataElseLoad` are `readonly`; they were public mutable statics.\n- Registered listeners no longer survive Play Mode. With Domain Reload disabled Unity keeps static fields between runs, so the listeners a previous run registered were still there for the next one. The SDK clears them at `RuntimeInitializeLoadType.SubsystemRegistration`.\n- **Breaking:** every concrete public class is now `sealed`; the four abstract roots the wire contract needs — `AdaptyCustomAsset` and the three legacy onboarding hierarchies — stay open. For most of them this states what was already true: a response model has no constructor reachable from outside the SDK — private, or `internal` as on `AdaptySubscriptionOffer` — so no type of yours could derive from one in the first place. Eleven could, all of them inputs rather than responses — the parameter objects, the two identity types, the three builders — and for those this is a real restriction. Nothing was designed for extension: no model declares a `virtual` or `protected` member.\n- **Breaking:** `AdaptyInstallationStatus` is one sealed type carrying a `Status` and a `Details`, instead of a base class and the three subclasses `AdaptyInstallationStatusNotAvailable`, `AdaptyInstallationStatusNotDetermined` and `AdaptyInstallationStatusDetermined`, which are gone along with their public constructors. The new `AdaptyInstallationStatusType` names the same three states the contract lists, so a caller switches on a value rather than testing for a type. `Details` is non-null exactly when `Status` is `Determined`. Nothing about the wire format changes.\n- **Breaking:** `AdaptyFlowPaywall.ProductReference` is now `internal`. Its constructor was private and every one of its members was already `internal`, so no instance of it could be obtained or read from outside the SDK.\n\n### Fixed\n\n- The `respond` delegate of `IAdaptyUISystemRequestsHandler.FlowViewDidAskPermission` and the report callbacks of `IAdaptyUIObserverModeResolver` are safe to invoke from any thread: the SDK now sends the request they produce from the Unity main thread. On Android the bridge is JNI, which a C# worker thread cannot enter on Unity 2022.3 (measured on 2022.3.62f3), so answering a permission request from one threw and left the flow waiting for an answer that never went out; Unity 6 attaches the thread on demand and happened to work. iOS was unaffected.\n- Requests call back on a device even when the app never sets an event listener. The platform callback bridge was registered by the four listener setters and by nothing else, so an app that subscribes to no events got no completion handler called at all — `Activate` included — on either platform, and every iOS request additionally leaked the handle meant to carry its reply. It is now registered at player startup, before the first scene loads, so every call made from the MonoBehaviour lifecycle onwards is covered. Present since 3.x.\n- An exception thrown by the completion handler passed to the deprecated `Adapty.GetOnboarding` now arrives with the name of the call that raised it, and the original as `InnerException`, the way every other Adapty call already reported one. Only `GetOnboarding` was affected.\n- `AdaptyProfileParameters.SetBirthday` now sends the date the contract asks for. The key is declared `YYYY-MM-dd` and was built by hand from the parts, without padding, so 7 March 1990 went out as `1990-3-7`. Every birthday whose month or day is below the tenth was affected, on every platform, since 3.x.\n- An offer whose branch of the contract requires an `id` is now rejected without one, at the point it is read: promotional and win-back everywhere, and introductory on Android. The converter read the identifier leniently for every branch, so a missing one became a null that `NullValueHandling` dropped from the purchase request, leaving the native side to fail the decode instead.\n- A subscription offer without `phases` is now rejected instead of being handed over half built. The contract requires the key, and the converter enforced `offer_identifier` and `type` but not this one, so a payload without it produced an offer whose phases were null. Neither native SDK can currently send such a payload.\n- Dates reach your code as local time again, as they did in 3.x. The wire is UTC and the public API is local — a subscription's expiry compares against `DateTime.Now` — but the payload from the native side was being turned into a document by a reader that recognises dates while it builds the tree, so every date arrived as `DateTimeKind.Utc`. Both the event callbacks and the reply to every method were affected; only 4.0.0-beta.1 ever behaved that way. Call `ToUniversalTime()` where you need the instant.\n- A date-looking string inside an untyped payload survives as it was sent. The same reader turned `params` of `FlowViewDidReceiveAnalyticEvent` into dates and back into strings, so `\"2026-07-30T10:00:00.000Z\"` reached the listener as `\"07/30/2026 10:00:00\"`.\n- `AdaptyPurchaseResult.ToString()` no longer throws. The contract carries `profile` in the success branch only, and the method dereferenced it unconditionally, so describing a pending or cancelled purchase raised a `NullReferenceException`.\n- `ReportTransaction` no longer reports `DecodingFailed` for transactions that were in fact reported successfully.\n- **The server cluster selected through the configuration builder now reaches the native SDK.** `ServerCluster` was the one builder field `AdaptyConfiguration`'s constructor did not copy, so `server_cluster` was never sent and every app ran against the default cluster whatever it chose. Selecting EU or CN did nothing in v3 and takes effect now, so an app that selected one starts talking to that region on upgrade. The builder field is `AdaptyServerCluster?` rather than `AdaptyServerCluster`, which keeps an unset cluster out of the request.\n- `AdaptyPlacementFetchPolicy.Default` is no longer null. It aliases `ReloadRevalidatingCacheData` but was declared above it, and static field initializers run in declaration order, so passing `Default` explicitly raised a `NullReferenceException` when the request was built.\n- `UpdateAttribution` sends `bool` and `DateTime` values instead of dropping them. The dictionary serializer had no branch for either, so an attribution carrying one went out without it.\n- `AdaptyCustomerIdentity.IsEmpty` reports an empty identity. `IosAppAccountToken` is a non-nullable `Guid`, so comparing it to null was always false and the property never returned true, which left the guard that keeps an empty identity out of the configuration dead.\n- A partially filled date in an onboarding `date_picker` event no longer throws. The helpers reading the optional `day`, `month` and `year` cast the nullable they received straight to `int`, which raises `InvalidOperationException` when the key is absent.\n- Custom linear gradient assets are now serialized from every color and alpha key of the Unity `Gradient`.\n- `AdaptyProductIdentifier` now implements value equality, so identifiers built from a flow work as keys in the dictionary passed to `AdaptyUICreateFlowViewParameters.SetProductPurchaseParameters`. An empty base plan id is now the same as none, at construction, so two identifiers that always went on the wire identically are equal and hash alike.\n- `AdaptyFlow.VendorProductIds` and `AdaptyFlow.ProductIdentifiers` no longer return duplicates across paywall variations.\n- Calling the SDK in the Editor now returns a readable \"not supported on this platform\" error. That holds for the whole surface now: `UpdateAppStoreCollectingRefundDataConsent`, `UpdateAppStoreRefundPreference` and `PresentCodeRedemptionSheet` were guarded so that the Editor took their off-iOS branch, which reports a null error — indistinguishable from success — so testing them in the Editor looked like they had worked. On an Android device they still report `null`, which is unchanged.\n- `UpdateAttribution` reports an attribution graph it cannot encode through the completion handler, as `EncodingFailed`, instead of throwing at the call site. The overload taking a dictionary is the only public method that has to encode an argument before it can build a request, so it was the only one whose failure escaped the transport's guard.\n- **Adapty SDK > Install Dependencies** stops on two loaded copies of Newtonsoft.Json, which is the state the SDK's own validator already reports as an error. It examined the first copy only, and the order loaded assemblies come back in is not specified, so the same project could be told its dependencies were complete on one run and be sent to fix them on the next.\n- **Adapty SDK > Install Dependencies** upgrades an External Dependency Manager older than the SDK needs, instead of reporting the project complete. It checked only that a copy was loaded, so a project coming from v3 — which declared 1.2.187 — kept it, and the iOS build resolved through a version that gets the Xcode project path wrong for the Swift project type. A copy installed from Google's own `.unitypackage` under `Assets/` has no version Package Manager can read, so it is reported rather than replaced.\n- `AdaptyConfiguration.Builder.ToString()` includes `GoogleEnablePendingPrepaidPlans`, the one member missing from the description.\n\n### Known issues\n\n- **Custom color and linear gradient assets are not rendered on iOS.** The pinned AdaptySDK-iOS 4.0.2 discards the values it receives and substitutes a transparent color and an empty gradient. Nothing on the Unity side is involved, and the same substitution is in the later 4.0.3 and 4.1.0 native releases, so there is no version to move the pin to. Custom image and video assets are unaffected. Whether Android is affected has not been established." + }, + "dependencies": { + "com.unity.nuget.newtonsoft-json": "3.2.2" }, "peerDependencies": { "com.google.external-dependency-manager": "1.2.188" diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index daf89c0..8caba23 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -4,7 +4,9 @@ "version": "file:com.adapty.unity-sdk", "depth": 0, "source": "embedded", - "dependencies": {} + "dependencies": { + "com.unity.nuget.newtonsoft-json": "3.2.2" + } }, "com.google.external-dependency-manager": { "version": "1.2.188", diff --git a/ProjectSettings/AudioManager.asset b/ProjectSettings/AudioManager.asset index 74ccc85..07ebfb0 100644 --- a/ProjectSettings/AudioManager.asset +++ b/ProjectSettings/AudioManager.asset @@ -1,19 +1,19 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!11 &1 -AudioManager: - m_ObjectHideFlags: 0 - serializedVersion: 2 - m_Volume: 1 - Rolloff Scale: 1 - Doppler Factor: 1 - Default Speaker Mode: 2 - m_SampleRate: 0 - m_DSPBufferSize: 1024 - m_VirtualVoiceCount: 512 - m_RealVoiceCount: 32 - m_SpatializerPlugin: - m_AmbisonicDecoderPlugin: - m_DisableAudio: 0 - m_VirtualizeEffects: 1 - m_RequestedDSPBufferSize: 1024 +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!11 &1 +AudioManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Volume: 1 + Rolloff Scale: 1 + Doppler Factor: 1 + Default Speaker Mode: 2 + m_SampleRate: 0 + m_DSPBufferSize: 1024 + m_VirtualVoiceCount: 512 + m_RealVoiceCount: 32 + m_SpatializerPlugin: + m_AmbisonicDecoderPlugin: + m_DisableAudio: 0 + m_VirtualizeEffects: 1 + m_RequestedDSPBufferSize: 1024 diff --git a/ProjectSettings/ClusterInputManager.asset b/ProjectSettings/ClusterInputManager.asset index a84cf4e..e7886b2 100644 --- a/ProjectSettings/ClusterInputManager.asset +++ b/ProjectSettings/ClusterInputManager.asset @@ -1,6 +1,6 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!236 &1 -ClusterInputManager: - m_ObjectHideFlags: 0 - m_Inputs: [] +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!236 &1 +ClusterInputManager: + m_ObjectHideFlags: 0 + m_Inputs: [] diff --git a/ProjectSettings/DynamicsManager.asset b/ProjectSettings/DynamicsManager.asset index 53f3851..cdc1f3e 100644 --- a/ProjectSettings/DynamicsManager.asset +++ b/ProjectSettings/DynamicsManager.asset @@ -1,34 +1,34 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!55 &1 -PhysicsManager: - m_ObjectHideFlags: 0 - serializedVersion: 11 - m_Gravity: {x: 0, y: -9.81, z: 0} - m_DefaultMaterial: {fileID: 0} - m_BounceThreshold: 2 - m_SleepThreshold: 0.005 - m_DefaultContactOffset: 0.01 - m_DefaultSolverIterations: 6 - m_DefaultSolverVelocityIterations: 1 - m_QueriesHitBackfaces: 0 - m_QueriesHitTriggers: 1 - m_EnableAdaptiveForce: 0 - m_ClothInterCollisionDistance: 0 - m_ClothInterCollisionStiffness: 0 - m_ContactsGeneration: 1 - m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - m_AutoSimulation: 1 - m_AutoSyncTransforms: 0 - m_ReuseCollisionCallbacks: 1 - m_ClothInterCollisionSettingsToggle: 0 - m_ContactPairsMode: 0 - m_BroadphaseType: 0 - m_WorldBounds: - m_Center: {x: 0, y: 0, z: 0} - m_Extent: {x: 250, y: 250, z: 250} - m_WorldSubdivisions: 8 - m_FrictionType: 0 - m_EnableEnhancedDeterminism: 0 - m_EnableUnifiedHeightmaps: 1 - m_DefaultMaxAngluarSpeed: 7 +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!55 &1 +PhysicsManager: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_Gravity: {x: 0, y: -9.81, z: 0} + m_DefaultMaterial: {fileID: 0} + m_BounceThreshold: 2 + m_SleepThreshold: 0.005 + m_DefaultContactOffset: 0.01 + m_DefaultSolverIterations: 6 + m_DefaultSolverVelocityIterations: 1 + m_QueriesHitBackfaces: 0 + m_QueriesHitTriggers: 1 + m_EnableAdaptiveForce: 0 + m_ClothInterCollisionDistance: 0 + m_ClothInterCollisionStiffness: 0 + m_ContactsGeneration: 1 + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_AutoSimulation: 1 + m_AutoSyncTransforms: 0 + m_ReuseCollisionCallbacks: 1 + m_ClothInterCollisionSettingsToggle: 0 + m_ContactPairsMode: 0 + m_BroadphaseType: 0 + m_WorldBounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 250, y: 250, z: 250} + m_WorldSubdivisions: 8 + m_FrictionType: 0 + m_EnableEnhancedDeterminism: 0 + m_EnableUnifiedHeightmaps: 1 + m_DefaultMaxAngluarSpeed: 7 diff --git a/ProjectSettings/EditorSettings.asset b/ProjectSettings/EditorSettings.asset index 5a7387f..7b02740 100644 --- a/ProjectSettings/EditorSettings.asset +++ b/ProjectSettings/EditorSettings.asset @@ -23,8 +23,8 @@ EditorSettings: m_EnableTextureStreamingInEditMode: 1 m_EnableTextureStreamingInPlayMode: 1 m_AsyncShaderCompilation: 1 - m_EnterPlayModeOptionsEnabled: 0 - m_EnterPlayModeOptions: 3 + m_EnterPlayModeOptionsEnabled: 1 + m_EnterPlayModeOptions: 1 m_ShowLightmapResolutionOverlay: 1 m_UseLegacyProbeSampleCount: 1 m_AssetPipelineMode: 1 diff --git a/ProjectSettings/InputManager.asset b/ProjectSettings/InputManager.asset index 2596646..17c8f53 100644 --- a/ProjectSettings/InputManager.asset +++ b/ProjectSettings/InputManager.asset @@ -1,295 +1,295 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!13 &1 -InputManager: - m_ObjectHideFlags: 0 - serializedVersion: 2 - m_Axes: - - serializedVersion: 3 - m_Name: Horizontal - descriptiveName: - descriptiveNegativeName: - negativeButton: left - positiveButton: right - altNegativeButton: a - altPositiveButton: d - gravity: 3 - dead: 0.001 - sensitivity: 3 - snap: 1 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Vertical - descriptiveName: - descriptiveNegativeName: - negativeButton: down - positiveButton: up - altNegativeButton: s - altPositiveButton: w - gravity: 3 - dead: 0.001 - sensitivity: 3 - snap: 1 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Fire1 - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: left ctrl - altNegativeButton: - altPositiveButton: mouse 0 - gravity: 1000 - dead: 0.001 - sensitivity: 1000 - snap: 0 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Fire2 - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: left alt - altNegativeButton: - altPositiveButton: mouse 1 - gravity: 1000 - dead: 0.001 - sensitivity: 1000 - snap: 0 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Fire3 - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: left shift - altNegativeButton: - altPositiveButton: mouse 2 - gravity: 1000 - dead: 0.001 - sensitivity: 1000 - snap: 0 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Jump - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: space - altNegativeButton: - altPositiveButton: - gravity: 1000 - dead: 0.001 - sensitivity: 1000 - snap: 0 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Mouse X - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: - altNegativeButton: - altPositiveButton: - gravity: 0 - dead: 0 - sensitivity: 0.1 - snap: 0 - invert: 0 - type: 1 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Mouse Y - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: - altNegativeButton: - altPositiveButton: - gravity: 0 - dead: 0 - sensitivity: 0.1 - snap: 0 - invert: 0 - type: 1 - axis: 1 - joyNum: 0 - - serializedVersion: 3 - m_Name: Mouse ScrollWheel - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: - altNegativeButton: - altPositiveButton: - gravity: 0 - dead: 0 - sensitivity: 0.1 - snap: 0 - invert: 0 - type: 1 - axis: 2 - joyNum: 0 - - serializedVersion: 3 - m_Name: Horizontal - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: - altNegativeButton: - altPositiveButton: - gravity: 0 - dead: 0.19 - sensitivity: 1 - snap: 0 - invert: 0 - type: 2 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Vertical - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: - altNegativeButton: - altPositiveButton: - gravity: 0 - dead: 0.19 - sensitivity: 1 - snap: 0 - invert: 1 - type: 2 - axis: 1 - joyNum: 0 - - serializedVersion: 3 - m_Name: Fire1 - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: joystick button 0 - altNegativeButton: - altPositiveButton: - gravity: 1000 - dead: 0.001 - sensitivity: 1000 - snap: 0 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Fire2 - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: joystick button 1 - altNegativeButton: - altPositiveButton: - gravity: 1000 - dead: 0.001 - sensitivity: 1000 - snap: 0 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Fire3 - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: joystick button 2 - altNegativeButton: - altPositiveButton: - gravity: 1000 - dead: 0.001 - sensitivity: 1000 - snap: 0 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Jump - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: joystick button 3 - altNegativeButton: - altPositiveButton: - gravity: 1000 - dead: 0.001 - sensitivity: 1000 - snap: 0 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Submit - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: return - altNegativeButton: - altPositiveButton: joystick button 0 - gravity: 1000 - dead: 0.001 - sensitivity: 1000 - snap: 0 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Submit - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: enter - altNegativeButton: - altPositiveButton: space - gravity: 1000 - dead: 0.001 - sensitivity: 1000 - snap: 0 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 - - serializedVersion: 3 - m_Name: Cancel - descriptiveName: - descriptiveNegativeName: - negativeButton: - positiveButton: escape - altNegativeButton: - altPositiveButton: joystick button 1 - gravity: 1000 - dead: 0.001 - sensitivity: 1000 - snap: 0 - invert: 0 - type: 0 - axis: 0 - joyNum: 0 +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!13 &1 +InputManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Axes: + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: a + altPositiveButton: d + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: s + altPositiveButton: w + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left ctrl + altNegativeButton: + altPositiveButton: mouse 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left alt + altNegativeButton: + altPositiveButton: mouse 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left shift + altNegativeButton: + altPositiveButton: mouse 2 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: space + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse X + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse Y + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse ScrollWheel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 2 + joyNum: 0 + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 0 + type: 2 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 1 + type: 2 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 0 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 1 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 2 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 3 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: return + altNegativeButton: + altPositiveButton: joystick button 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: enter + altNegativeButton: + altPositiveButton: space + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Cancel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: escape + altNegativeButton: + altPositiveButton: joystick button 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 diff --git a/ProjectSettings/NavMeshAreas.asset b/ProjectSettings/NavMeshAreas.asset index c8fa1b5..3b0b7c3 100644 --- a/ProjectSettings/NavMeshAreas.asset +++ b/ProjectSettings/NavMeshAreas.asset @@ -1,91 +1,91 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!126 &1 -NavMeshProjectSettings: - m_ObjectHideFlags: 0 - serializedVersion: 2 - areas: - - name: Walkable - cost: 1 - - name: Not Walkable - cost: 1 - - name: Jump - cost: 2 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - - name: - cost: 1 - m_LastAgentTypeID: -887442657 - m_Settings: - - serializedVersion: 2 - agentTypeID: 0 - agentRadius: 0.5 - agentHeight: 2 - agentSlope: 45 - agentClimb: 0.75 - ledgeDropHeight: 0 - maxJumpAcrossDistance: 0 - minRegionArea: 2 - manualCellSize: 0 - cellSize: 0.16666667 - manualTileSize: 0 - tileSize: 256 - accuratePlacement: 0 - debug: - m_Flags: 0 - m_SettingNames: - - Humanoid +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!126 &1 +NavMeshProjectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + areas: + - name: Walkable + cost: 1 + - name: Not Walkable + cost: 1 + - name: Jump + cost: 2 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + m_LastAgentTypeID: -887442657 + m_Settings: + - serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.75 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + debug: + m_Flags: 0 + m_SettingNames: + - Humanoid diff --git a/ProjectSettings/NetworkManager.asset b/ProjectSettings/NetworkManager.asset index e9cd578..5dc6a83 100644 --- a/ProjectSettings/NetworkManager.asset +++ b/ProjectSettings/NetworkManager.asset @@ -1,8 +1,8 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!149 &1 -NetworkManager: - m_ObjectHideFlags: 0 - m_DebugLevel: 0 - m_Sendrate: 15 - m_AssetToPrefab: {} +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!149 &1 +NetworkManager: + m_ObjectHideFlags: 0 + m_DebugLevel: 0 + m_Sendrate: 15 + m_AssetToPrefab: {} diff --git a/ProjectSettings/Physics2DSettings.asset b/ProjectSettings/Physics2DSettings.asset index 1a546aa..47880b1 100644 --- a/ProjectSettings/Physics2DSettings.asset +++ b/ProjectSettings/Physics2DSettings.asset @@ -1,56 +1,56 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!19 &1 -Physics2DSettings: - m_ObjectHideFlags: 0 - serializedVersion: 4 - m_Gravity: {x: 0, y: -9.81} - m_DefaultMaterial: {fileID: 0} - m_VelocityIterations: 8 - m_PositionIterations: 3 - m_VelocityThreshold: 1 - m_MaxLinearCorrection: 0.2 - m_MaxAngularCorrection: 8 - m_MaxTranslationSpeed: 100 - m_MaxRotationSpeed: 360 - m_BaumgarteScale: 0.2 - m_BaumgarteTimeOfImpactScale: 0.75 - m_TimeToSleep: 0.5 - m_LinearSleepTolerance: 0.01 - m_AngularSleepTolerance: 2 - m_DefaultContactOffset: 0.01 - m_JobOptions: - serializedVersion: 2 - useMultithreading: 0 - useConsistencySorting: 0 - m_InterpolationPosesPerJob: 100 - m_NewContactsPerJob: 30 - m_CollideContactsPerJob: 100 - m_ClearFlagsPerJob: 200 - m_ClearBodyForcesPerJob: 200 - m_SyncDiscreteFixturesPerJob: 50 - m_SyncContinuousFixturesPerJob: 50 - m_FindNearestContactsPerJob: 100 - m_UpdateTriggerContactsPerJob: 100 - m_IslandSolverCostThreshold: 100 - m_IslandSolverBodyCostScale: 1 - m_IslandSolverContactCostScale: 10 - m_IslandSolverJointCostScale: 10 - m_IslandSolverBodiesPerJob: 50 - m_IslandSolverContactsPerJob: 50 - m_AutoSimulation: 1 - m_QueriesHitTriggers: 1 - m_QueriesStartInColliders: 1 - m_CallbacksOnDisable: 1 - m_ReuseCollisionCallbacks: 1 - m_AutoSyncTransforms: 0 - m_AlwaysShowColliders: 0 - m_ShowColliderSleep: 1 - m_ShowColliderContacts: 0 - m_ShowColliderAABB: 0 - m_ContactArrowScale: 0.2 - m_ColliderAwakeColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.7529412} - m_ColliderAsleepColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.36078432} - m_ColliderContactColor: {r: 1, g: 0, b: 1, a: 0.6862745} - m_ColliderAABBColor: {r: 1, g: 1, b: 0, a: 0.2509804} - m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!19 &1 +Physics2DSettings: + m_ObjectHideFlags: 0 + serializedVersion: 4 + m_Gravity: {x: 0, y: -9.81} + m_DefaultMaterial: {fileID: 0} + m_VelocityIterations: 8 + m_PositionIterations: 3 + m_VelocityThreshold: 1 + m_MaxLinearCorrection: 0.2 + m_MaxAngularCorrection: 8 + m_MaxTranslationSpeed: 100 + m_MaxRotationSpeed: 360 + m_BaumgarteScale: 0.2 + m_BaumgarteTimeOfImpactScale: 0.75 + m_TimeToSleep: 0.5 + m_LinearSleepTolerance: 0.01 + m_AngularSleepTolerance: 2 + m_DefaultContactOffset: 0.01 + m_JobOptions: + serializedVersion: 2 + useMultithreading: 0 + useConsistencySorting: 0 + m_InterpolationPosesPerJob: 100 + m_NewContactsPerJob: 30 + m_CollideContactsPerJob: 100 + m_ClearFlagsPerJob: 200 + m_ClearBodyForcesPerJob: 200 + m_SyncDiscreteFixturesPerJob: 50 + m_SyncContinuousFixturesPerJob: 50 + m_FindNearestContactsPerJob: 100 + m_UpdateTriggerContactsPerJob: 100 + m_IslandSolverCostThreshold: 100 + m_IslandSolverBodyCostScale: 1 + m_IslandSolverContactCostScale: 10 + m_IslandSolverJointCostScale: 10 + m_IslandSolverBodiesPerJob: 50 + m_IslandSolverContactsPerJob: 50 + m_AutoSimulation: 1 + m_QueriesHitTriggers: 1 + m_QueriesStartInColliders: 1 + m_CallbacksOnDisable: 1 + m_ReuseCollisionCallbacks: 1 + m_AutoSyncTransforms: 0 + m_AlwaysShowColliders: 0 + m_ShowColliderSleep: 1 + m_ShowColliderContacts: 0 + m_ShowColliderAABB: 0 + m_ContactArrowScale: 0.2 + m_ColliderAwakeColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.7529412} + m_ColliderAsleepColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.36078432} + m_ColliderContactColor: {r: 1, g: 0, b: 1, a: 0.6862745} + m_ColliderAABBColor: {r: 1, g: 1, b: 0, a: 0.2509804} + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff diff --git a/ProjectSettings/PresetManager.asset b/ProjectSettings/PresetManager.asset index 7d66f80..67a94da 100644 --- a/ProjectSettings/PresetManager.asset +++ b/ProjectSettings/PresetManager.asset @@ -1,7 +1,7 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!1386491679 &1 -PresetManager: - m_ObjectHideFlags: 0 - serializedVersion: 2 - m_DefaultPresets: {} +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1386491679 &1 +PresetManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_DefaultPresets: {} diff --git a/ProjectSettings/QualitySettings.asset b/ProjectSettings/QualitySettings.asset index 698f64f..84c1610 100644 --- a/ProjectSettings/QualitySettings.asset +++ b/ProjectSettings/QualitySettings.asset @@ -1,192 +1,192 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!47 &1 -QualitySettings: - m_ObjectHideFlags: 0 - serializedVersion: 5 - m_CurrentQuality: 3 - m_QualitySettings: - - serializedVersion: 2 - name: Very Low - pixelLightCount: 0 - shadows: 0 - shadowResolution: 0 - shadowProjection: 1 - shadowCascades: 1 - shadowDistance: 15 - shadowNearPlaneOffset: 3 - shadowCascade2Split: 0.33333334 - shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} - shadowmaskMode: 0 - blendWeights: 1 - textureQuality: 1 - anisotropicTextures: 0 - antiAliasing: 0 - softParticles: 0 - softVegetation: 0 - realtimeReflectionProbes: 0 - billboardsFaceCameraPosition: 0 - vSyncCount: 0 - lodBias: 0.3 - maximumLODLevel: 0 - particleRaycastBudget: 4 - asyncUploadTimeSlice: 2 - asyncUploadBufferSize: 16 - resolutionScalingFixedDPIFactor: 1 - excludedTargetPlatforms: [] - - serializedVersion: 2 - name: Low - pixelLightCount: 0 - shadows: 0 - shadowResolution: 0 - shadowProjection: 1 - shadowCascades: 1 - shadowDistance: 20 - shadowNearPlaneOffset: 3 - shadowCascade2Split: 0.33333334 - shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} - shadowmaskMode: 0 - blendWeights: 2 - textureQuality: 0 - anisotropicTextures: 0 - antiAliasing: 0 - softParticles: 0 - softVegetation: 0 - realtimeReflectionProbes: 0 - billboardsFaceCameraPosition: 0 - vSyncCount: 0 - lodBias: 0.4 - maximumLODLevel: 0 - particleRaycastBudget: 16 - asyncUploadTimeSlice: 2 - asyncUploadBufferSize: 16 - resolutionScalingFixedDPIFactor: 1 - excludedTargetPlatforms: [] - - serializedVersion: 2 - name: Medium - pixelLightCount: 1 - shadows: 0 - shadowResolution: 0 - shadowProjection: 1 - shadowCascades: 1 - shadowDistance: 20 - shadowNearPlaneOffset: 3 - shadowCascade2Split: 0.33333334 - shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} - shadowmaskMode: 0 - blendWeights: 2 - textureQuality: 0 - anisotropicTextures: 0 - antiAliasing: 0 - softParticles: 0 - softVegetation: 0 - realtimeReflectionProbes: 0 - billboardsFaceCameraPosition: 0 - vSyncCount: 1 - lodBias: 0.7 - maximumLODLevel: 0 - particleRaycastBudget: 64 - asyncUploadTimeSlice: 2 - asyncUploadBufferSize: 16 - resolutionScalingFixedDPIFactor: 1 - excludedTargetPlatforms: [] - - serializedVersion: 2 - name: High - pixelLightCount: 2 - shadows: 0 - shadowResolution: 1 - shadowProjection: 1 - shadowCascades: 2 - shadowDistance: 40 - shadowNearPlaneOffset: 3 - shadowCascade2Split: 0.33333334 - shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} - shadowmaskMode: 1 - blendWeights: 2 - textureQuality: 0 - anisotropicTextures: 0 - antiAliasing: 0 - softParticles: 0 - softVegetation: 1 - realtimeReflectionProbes: 0 - billboardsFaceCameraPosition: 0 - vSyncCount: 1 - lodBias: 1 - maximumLODLevel: 0 - particleRaycastBudget: 256 - asyncUploadTimeSlice: 2 - asyncUploadBufferSize: 16 - resolutionScalingFixedDPIFactor: 1 - excludedTargetPlatforms: [] - - serializedVersion: 2 - name: Very High - pixelLightCount: 3 - shadows: 0 - shadowResolution: 2 - shadowProjection: 1 - shadowCascades: 2 - shadowDistance: 70 - shadowNearPlaneOffset: 3 - shadowCascade2Split: 0.33333334 - shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} - shadowmaskMode: 1 - blendWeights: 4 - textureQuality: 0 - anisotropicTextures: 0 - antiAliasing: 0 - softParticles: 0 - softVegetation: 1 - realtimeReflectionProbes: 0 - billboardsFaceCameraPosition: 0 - vSyncCount: 1 - lodBias: 1.5 - maximumLODLevel: 0 - particleRaycastBudget: 1024 - asyncUploadTimeSlice: 2 - asyncUploadBufferSize: 16 - resolutionScalingFixedDPIFactor: 1 - excludedTargetPlatforms: [] - - serializedVersion: 2 - name: Ultra - pixelLightCount: 4 - shadows: 0 - shadowResolution: 0 - shadowProjection: 1 - shadowCascades: 4 - shadowDistance: 150 - shadowNearPlaneOffset: 3 - shadowCascade2Split: 0.33333334 - shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} - shadowmaskMode: 1 - blendWeights: 4 - textureQuality: 0 - anisotropicTextures: 0 - antiAliasing: 0 - softParticles: 0 - softVegetation: 1 - realtimeReflectionProbes: 0 - billboardsFaceCameraPosition: 0 - vSyncCount: 1 - lodBias: 2 - maximumLODLevel: 0 - particleRaycastBudget: 4096 - asyncUploadTimeSlice: 2 - asyncUploadBufferSize: 16 - resolutionScalingFixedDPIFactor: 1 - excludedTargetPlatforms: [] - m_PerPlatformDefaultQuality: - Android: 2 - Nintendo 3DS: 5 - Nintendo Switch: 5 - PS4: 5 - PSM: 5 - PSP2: 2 - Stadia: 5 - Standalone: 5 - Tizen: 2 - WebGL: 3 - WiiU: 5 - Windows Store Apps: 5 - XboxOne: 5 - iPhone: 2 - tvOS: 2 +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!47 &1 +QualitySettings: + m_ObjectHideFlags: 0 + serializedVersion: 5 + m_CurrentQuality: 3 + m_QualitySettings: + - serializedVersion: 2 + name: Very Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 15 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 1 + textureQuality: 1 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.3 + maximumLODLevel: 0 + particleRaycastBudget: 4 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.4 + maximumLODLevel: 0 + particleRaycastBudget: 16 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Medium + pixelLightCount: 1 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 0.7 + maximumLODLevel: 0 + particleRaycastBudget: 64 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: High + pixelLightCount: 2 + shadows: 0 + shadowResolution: 1 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 40 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 1 + maximumLODLevel: 0 + particleRaycastBudget: 256 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Very High + pixelLightCount: 3 + shadows: 0 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 70 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 4 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 1.5 + maximumLODLevel: 0 + particleRaycastBudget: 1024 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Ultra + pixelLightCount: 4 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 4 + shadowDistance: 150 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 4 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 2 + maximumLODLevel: 0 + particleRaycastBudget: 4096 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + m_PerPlatformDefaultQuality: + Android: 2 + Nintendo 3DS: 5 + Nintendo Switch: 5 + PS4: 5 + PSM: 5 + PSP2: 2 + Stadia: 5 + Standalone: 5 + Tizen: 2 + WebGL: 3 + WiiU: 5 + Windows Store Apps: 5 + XboxOne: 5 + iPhone: 2 + tvOS: 2 diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset index 3281f1b..1c92a78 100644 --- a/ProjectSettings/TagManager.asset +++ b/ProjectSettings/TagManager.asset @@ -1,43 +1,43 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!78 &1 -TagManager: - serializedVersion: 2 - tags: [] - layers: - - Default - - TransparentFX - - Ignore Raycast - - - - Water - - UI - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - m_SortingLayers: - - name: Default - uniqueID: 0 - locked: 0 +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!78 &1 +TagManager: + serializedVersion: 2 + tags: [] + layers: + - Default + - TransparentFX + - Ignore Raycast + - + - Water + - UI + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + m_SortingLayers: + - name: Default + uniqueID: 0 + locked: 0 diff --git a/ProjectSettings/TimeManager.asset b/ProjectSettings/TimeManager.asset index b816de4..06bcc6d 100644 --- a/ProjectSettings/TimeManager.asset +++ b/ProjectSettings/TimeManager.asset @@ -1,9 +1,9 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!5 &1 -TimeManager: - m_ObjectHideFlags: 0 - Fixed Timestep: 0.02 - Maximum Allowed Timestep: 0.1 - m_TimeScale: 1 - Maximum Particle Timestep: 0.03 +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!5 &1 +TimeManager: + m_ObjectHideFlags: 0 + Fixed Timestep: 0.02 + Maximum Allowed Timestep: 0.1 + m_TimeScale: 1 + Maximum Particle Timestep: 0.03 diff --git a/ProjectSettings/VFXManager.asset b/ProjectSettings/VFXManager.asset index 379de33..3a95c98 100644 --- a/ProjectSettings/VFXManager.asset +++ b/ProjectSettings/VFXManager.asset @@ -1,12 +1,12 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!937362698 &1 -VFXManager: - m_ObjectHideFlags: 0 - m_IndirectShader: {fileID: 0} - m_CopyBufferShader: {fileID: 0} - m_SortShader: {fileID: 0} - m_StripUpdateShader: {fileID: 0} - m_RenderPipeSettingsPath: - m_FixedTimeStep: 0.016666668 - m_MaxDeltaTime: 0.05 +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!937362698 &1 +VFXManager: + m_ObjectHideFlags: 0 + m_IndirectShader: {fileID: 0} + m_CopyBufferShader: {fileID: 0} + m_SortShader: {fileID: 0} + m_StripUpdateShader: {fileID: 0} + m_RenderPipeSettingsPath: + m_FixedTimeStep: 0.016666668 + m_MaxDeltaTime: 0.05 diff --git a/README.md b/README.md index 27597c2..a6dd910 100755 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

- +

@@ -21,7 +21,13 @@ ![Adapty: CRM for mobile apps with subscriptions](https://adapty-portal-media-production.s3.amazonaws.com/github/adapty-schema.png) -Adapty Unity SDK is a native wrapper around [Adapty iOS SDK](https://github.com/adaptyteam/AdaptySDK-iOS) and [Adapty Android SDK](https://github.com/adaptyteam/AdaptySDK-Android). Both SDKs are written in pure Swift/Kotlin and support iOS 9+, Android 4.1+ which fit for 99.9% users all wrapped into C# lib. +Adapty Unity SDK is a native wrapper around [Adapty iOS SDK](https://github.com/adaptyteam/AdaptySDK-iOS) and [Adapty Android SDK](https://github.com/adaptyteam/AdaptySDK-Android). Both SDKs are written in pure Swift/Kotlin, all wrapped into a C# lib. + +Requires Unity 2022.3 or newer and Android API 21 or newer. iOS builds require **Xcode 26 or newer** +and a deployment target of 15.0 or newer: AdaptySDK-iOS 4.0 is a `swift-tools-version: 6.2` package, +and an older toolchain refuses to resolve it. Open the exported project as +**`Unity-iPhone.xcworkspace`** — building `Unity-iPhone.xcodeproj` directly fails at link time with +`ld: framework 'Pods_UnityFramework' not found`. ## Why Adapty? @@ -41,7 +47,7 @@ Adapty Unity SDK is a native wrapper around [Adapty iOS SDK](https://github.com/ **Adapty handles everything, from free trials to refunds, in a simple, developer-friendly SDK.** -- Free trials, upgrades, downgrades, crossgrades, family sharing, renewals, promo offers, intro offers, promo codes, and more – Adapty SDK does everything with a single line of code. +- Free trials, upgrades, downgrades, crossgrades, family sharing, renewals, promo offers, intro offers, promo codes, and more – Adapty SDK handles them all through one API. - Easy subscription management. - One-time purchases and lifetime subscriptions supported. - Sync subscribers' states across iOS, Android, and Web. @@ -78,7 +84,57 @@ Ask questions, participate in discussions about Adapty-related topics, become a ## Get started -Follow our [quickstart guide](https://adapty.io/docs/unity-sdk-overview#get-started?utm_source=github&utm_medium=referral&utm_campaign=AdaptySDK-Unity) to install and configure Adapty SDK. Set up purchases in hours instead of weeks 🚀 +Follow our [quickstart guide](https://adapty.io/docs/unity-sdk-overview?utm_source=github&utm_medium=referral&utm_campaign=AdaptySDK-Unity#get-started) to install and configure Adapty SDK. Set up purchases in hours instead of weeks 🚀 + +**v4 works in flows.** Paywalls and onboardings are both fetched with `Adapty.GetFlow` and shown +with `AdaptyUI.CreateFlowView`, whether you built them in the Paywall Builder or the new Flow +Builder. The separate onboarding API of v3 still works but is deprecated and warns at compile time, +so start new integrations on flows. + +**Installing with Package Manager:** *Add package from git URL*, with the path suffix — the package +does not sit at the repository root — and the version suffix, which pins the tag: + +``` +https://github.com/adaptyteam/AdaptySDK-Unity.git?path=/Packages/com.adapty.unity-sdk#4.0.0-beta.2 +``` + +Drop `#4.0.0-beta.2` and Package Manager resolves the default branch, which carries the previous +major until a release merges this one into it — so an unpinned URL installs 3.17 and says nothing +about it. + +The SDK depends on `com.unity.nuget.newtonsoft-json`, which Package Manager installs for you and +which every platform needs — the SDK assembly is gated on it. It also depends on External Dependency +Manager, but only for iOS, where it resolves the Swift package; Android never goes through it, since +its dependencies ship in a bundled `.androidlib` that Unity adds to the Gradle build itself. Neither +dependency can arrive with a `.unitypackage`, which carries assets only. + +**Installing from a `.unitypackage`:** take the latest from +[Releases](https://github.com/adaptyteam/AdaptySDK-Unity/releases), and add +`com.unity.nuget.newtonsoft-json` **before** importing. Until it is there the SDK assembly is skipped +by a define constraint, so your calls into Adapty will not compile. **Adapty SDK > Install +Dependencies** is what fixes that, and it lives in an Editor assembly carrying no such constraint — +so it is available as long as the rest of your scripts still compile. Once they do not, because they +are the code calling Adapty, Unity stops loading Editor assemblies and Newtonsoft has to come from +Package Manager by hand. With Newtonsoft in place, that menu item adds whatever else is missing, +including the OpenUPM scoped registry External Dependency Manager is published on. + +**Upgrading from 3.x: delete `Assets/AdaptySDK` before importing.** A `.unitypackage` never removes +files, and 4.0 drops 62 sources that 3.x shipped — the whole `JSON/` folder among them. Left where +they are, they compile into the same assembly as the new ones: 35 of them redeclare a type 4.0 also +declares, and the rest reference types it no longer has. Either way the SDK does not +compile, and because the assembly is gated on Newtonsoft the errors appear only once Newtonsoft is +installed. + +It also upgrades an External Dependency Manager older than the SDK needs — but only one installed as +a package. A copy imported from Google's own `.unitypackage` under `Assets/` has no version Package +Manager can read, so the menu item leaves it alone and warns instead; update that one yourself. + +Already on 3.x? [MIGRATION-v3.17-to-v4.0.md](MIGRATION-v3.17-to-v4.0.md) covers the move to 4.0 — the renamed paywall API, the +new Newtonsoft.Json dependency, and the order to install things in. + +Read the [release notes and known issues](Packages/com.adapty.unity-sdk/CHANGELOG.md) before you +integrate: they carry the limitations of the pinned native SDKs, which no amount of configuration on +your side will work around. ## Kids Mode on iOS @@ -89,7 +145,8 @@ Apps in the App Store Kids Category must not link the advertising identifier. Ad AdSupport and AppTrackingTransparency are compiled out of the binary; - `apple_idfa_collection_disabled` is forced in the runtime configuration. -Requires Xcode 26 or newer, which is where Swift package traits are supported. +Swift package traits need Xcode 26 or newer, which is already the floor for v4 on iOS — Kids Mode +adds no requirement of its own. Set the define in **Player Settings > Other Settings > Scripting Define Symbols**. The build step that enables the trait lives in an Editor assembly, and Player Settings is what Editor assemblies @@ -116,4 +173,18 @@ So do we! Feel free to star the repo ⭐️⭐️⭐️ and make our developers ## License -Adapty is available under the MIT license. [Click here](https://github.com/adaptyteam/AdaptySDK-Unity/blob/master/LICENSE) for details. +Adapty is available under the MIT license. [Click here](https://github.com/adaptyteam/AdaptySDK-Unity/blob/main/LICENSE) for details. + +## Known issues + +What is open in this release, and what closing it waits on. The +[changelog](Packages/com.adapty.unity-sdk/CHANGELOG.md) carries the full text of each under the +version it was found in. + +- **Custom color and linear gradient assets are not rendered on iOS.** An asset built with + `AdaptyCustomAsset.Color` or `AdaptyCustomAsset.LinearGradient` and passed through + `AdaptyUICreateFlowViewParameters.SetCustomAssets` reaches the view as a transparent color and an + empty gradient: the pinned AdaptySDK-iOS 4.0.2 substitutes those for whatever it receives, and so + do 4.0.3 and 4.1.0, so there is no version to move the pin to. Custom image and video assets are + unaffected; whether Android is affected has not been established. Waiting on a native iOS + release, and on iOS acceptance after it. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..beb14a6 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,35 @@ +# Third-party notices + +This repository contains material from the projects below. Their licences are reproduced here as +those licences require. The SDK's own licence is in [LICENSE](LICENSE) and is unaffected. + +## gitattributes/gitattributes + +`.gitattributes` is adapted from `Unity.gitattributes` in +. The macros, the Unity YAML and JSON pattern lists +and the file's structure come from there; the binary handling replaces the upstream Git LFS rules, +which this repository does not use. + +``` +MIT License + +Copyright (c) 2015-2026 Alexander Karatarakis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/adaptyandroidwrapper/gradlew.bat b/adaptyandroidwrapper/gradlew.bat index e95643d..f955316 100644 --- a/adaptyandroidwrapper/gradlew.bat +++ b/adaptyandroidwrapper/gradlew.bat @@ -1,84 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/adaptyandroidwrapper/unitywrapper/src/main/java/com/adapty/unity/AdaptyAndroidWrapper.java b/adaptyandroidwrapper/unitywrapper/src/main/java/com/adapty/unity/AdaptyAndroidWrapper.java index 0fad161..07ac728 100644 --- a/adaptyandroidwrapper/unitywrapper/src/main/java/com/adapty/unity/AdaptyAndroidWrapper.java +++ b/adaptyandroidwrapper/unitywrapper/src/main/java/com/adapty/unity/AdaptyAndroidWrapper.java @@ -45,10 +45,28 @@ public static CrossplatformHelper getHelper() { private static Handler unityMainThreadHandler; private static AdaptyAndroidMessageHandler messageHandler; + /** + * Binds to the registering thread's Looper - Unity's, since the C# side registers from its + * scripting thread before the first scene loads. Unity's player loop is not the Android UI + * thread, so Looper.getMainLooper() would deliver every callback on the wrong one. + */ public static void registerMessageHandler(AdaptyAndroidMessageHandler handler) { + if(unityMainThreadHandler == null) { + Looper looper = Looper.myLooper(); + if (looper == null) { + throw new IllegalStateException( + "Adapty: registerMessageHandler was called from a thread with no Looper, so SDK " + + "callbacks cannot be delivered back to it. It is expected to be called " + + "from Unity's scripting thread, which Adapty.InitializeTransport does " + + "before the first scene loads." + ); + } + unityMainThreadHandler = new Handler(looper); + } + + // Assigned only once there is a handler to deliver through. Set before the check, a failed + // registration would leave the wrapper holding a listener it can never call. messageHandler = handler; - if(unityMainThreadHandler == null) - unityMainThreadHandler = new Handler(Looper.getMainLooper()); } public static void runOnUnityThread(Runnable runnable) { diff --git a/deploy/AdaptyDemo.storekit b/deploy/AdaptyDemo.storekit new file mode 100644 index 0000000..d0ac2d3 --- /dev/null +++ b/deploy/AdaptyDemo.storekit @@ -0,0 +1,97 @@ +{ + "identifier": "AD2E4CA1-E1C6-4DD5-92EC-ADB9D3D6524E", + "nonRenewingSubscriptions": [], + "products": [], + "settings": { + "_failTransactionsEnabled": false, + "_askToBuyEnabled": false + }, + "subscriptionGroups": [ + { + "id": "7DD53B64-E911-4C1C-AC72-E7604A7AADAF", + "localizations": [], + "name": "premium", + "subscriptions": [ + { + "adHocOffers": [], + "codeOffers": [], + "displayPrice": "5.99", + "familyShareable": false, + "groupNumber": 1, + "internalID": "8FFAF2A4-9C49-4744-96C7-09C8A49CA15D", + "introductoryOffer": { + "internalID": "1E1E333C-6F4B-409C-8864-A08174ECBE55", + "paymentMode": "free", + "subscriptionPeriod": "P1W" + }, + "localizations": [ + { + "description": "1 Week Premium", + "displayName": "1 Week Premium", + "locale": "en_US" + } + ], + "productID": "weekly.premium.599", + "recurringSubscriptionPeriod": "P1W", + "referenceName": "1 Week Premium", + "subscriptionGroupID": "7DD53B64-E911-4C1C-AC72-E7604A7AADAF", + "type": "RecurringSubscription" + }, + { + "adHocOffers": [], + "codeOffers": [], + "displayPrice": "9.99", + "familyShareable": false, + "groupNumber": 2, + "internalID": "63F8B390-B7C0-49B6-AF90-51053C0FA917", + "introductoryOffer": { + "internalID": "370BB628-746C-4CB9-91E4-E275580B7EF4", + "paymentMode": "free", + "subscriptionPeriod": "P1W" + }, + "localizations": [ + { + "description": "1 Month Premium", + "displayName": "1 Month Premium", + "locale": "en_US" + } + ], + "productID": "monthly.premium.999", + "recurringSubscriptionPeriod": "P1M", + "referenceName": "1 Month Premium", + "subscriptionGroupID": "7DD53B64-E911-4C1C-AC72-E7604A7AADAF", + "type": "RecurringSubscription" + }, + { + "adHocOffers": [], + "codeOffers": [], + "displayPrice": "69.99", + "familyShareable": false, + "groupNumber": 3, + "internalID": "831CD323-BC49-45F9-A47F-C51A9EFDB177", + "introductoryOffer": { + "internalID": "5AE22388-BB92-4EDD-A9F0-E721F2EB5004", + "paymentMode": "free", + "subscriptionPeriod": "P1W" + }, + "localizations": [ + { + "description": "1 Year Premium", + "displayName": "1 Year Premium", + "locale": "en_US" + } + ], + "productID": "yearly.premium.6999", + "recurringSubscriptionPeriod": "P1Y", + "referenceName": "1 Year Premium", + "subscriptionGroupID": "7DD53B64-E911-4C1C-AC72-E7604A7AADAF", + "type": "RecurringSubscription" + } + ] + } + ], + "version": { + "major": 4, + "minor": 0 + } +} \ No newline at end of file diff --git a/deploy/build_unitypackage.sh b/deploy/build_unitypackage.sh index c8f0cab..6ba293d 100755 --- a/deploy/build_unitypackage.sh +++ b/deploy/build_unitypackage.sh @@ -7,6 +7,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" DEPLOY_PATH="$SCRIPT_DIR/output" SOURCE_PATH="$PROJECT_ROOT/Packages/com.adapty.unity-sdk/Runtime" +EDITOR_SOURCE_PATH="$PROJECT_ROOT/Packages/com.adapty.unity-sdk/Editor" PACKAGE_JSON="$PROJECT_ROOT/Packages/com.adapty.unity-sdk/package.json" STAGED_SDK_PATH="Assets/AdaptySDK" UNITY_PATH="${UNITY_PATH:-}" @@ -14,6 +15,11 @@ PACKAGE_NAME="${PACKAGE_NAME:-}" PACKAGE_VERSION="$(sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$PACKAGE_JSON" 2>/dev/null | head -n 1 || true)" DEFAULT_PACKAGE_NAME="adapty-unity-plugin-${PACKAGE_VERSION:-unknown}.unitypackage" +# The key is anchored to the start of a line so the copies inside the _upm.changelog string, which +# is one long line, cannot be read instead. +NEWTONSOFT_ID="com.unity.nuget.newtonsoft-json" +NEWTONSOFT_VERSION="$(sed -n "s|^[[:space:]]*\"$NEWTONSOFT_ID\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*|\1|p" "$PACKAGE_JSON" 2>/dev/null | head -n 1 || true)" + PRODUCTION=0 KEEP_STAGING=0 @@ -92,6 +98,11 @@ if [[ ! -d "$SOURCE_PATH" ]]; then exit 1 fi +if [[ ! -d "$EDITOR_SOURCE_PATH" ]]; then + echo "SDK editor sources not found: $EDITOR_SOURCE_PATH" >&2 + exit 1 +fi + if [[ ! -f "$PACKAGE_JSON" ]]; then echo "Package manifest not found: $PACKAGE_JSON" >&2 exit 1 @@ -102,6 +113,11 @@ if [[ -z "$PACKAGE_VERSION" ]]; then exit 1 fi +if [[ -z "$NEWTONSOFT_VERSION" ]]; then + echo "$NEWTONSOFT_ID not found in the dependencies of $PACKAGE_JSON" >&2 + exit 1 +fi + if [[ -z "$PACKAGE_NAME" ]]; then PACKAGE_NAME="$DEFAULT_PACKAGE_NAME" fi @@ -131,12 +147,22 @@ mkdir -p "$STAGING_PROJECT/Assets" mkdir -p "$STAGING_PROJECT/Packages" mkdir -p "$STAGING_PROJECT/ProjectSettings" -printf '{ "dependencies": {} }\n' > "$STAGING_PROJECT/Packages/manifest.json" +# The staged sources are compiled by the Editor during export, so the staging project has to +# resolve what they import. Newtonsoft arrived with the JSON layer migration; without it the +# export builds against a project that cannot compile the SDK. The version is read from the +# manifest rather than written here, so the export cannot be built against a version the package +# does not declare. +printf '{ "dependencies": { "%s": "%s" } }\n' "$NEWTONSOFT_ID" "$NEWTONSOFT_VERSION" \ + > "$STAGING_PROJECT/Packages/manifest.json" cp "$PROJECT_ROOT/ProjectSettings/ProjectVersion.txt" "$STAGING_PROJECT/ProjectSettings/ProjectVersion.txt" cp -R "$SOURCE_PATH" "$STAGING_PROJECT/$STAGED_SDK_PATH" cp "$SOURCE_PATH.meta" "$STAGING_PROJECT/$STAGED_SDK_PATH.meta" +# The package keeps its editor-only code outside Runtime, so it needs a second copy. It merges into +# the Editor folder Runtime already contributes, which carries AdaptySDKDependencies.xml. +cp -R "$EDITOR_SOURCE_PATH/." "$STAGING_PROJECT/$STAGED_SDK_PATH/Editor/" + EXPORT_PATH="$DEPLOY_PATH/$PACKAGE_NAME" LOG_PATH="$DEPLOY_PATH/build_unitypackage.log" diff --git a/deploy/release_unitypackage.sh b/deploy/release_unitypackage.sh index 5f66218..da48e6d 100755 --- a/deploy/release_unitypackage.sh +++ b/deploy/release_unitypackage.sh @@ -1,4 +1,28 @@ #!/usr/bin/env bash +# +# Publishes the .unitypackage: builds it, commits it under Releases/, tags, pushes, and creates the +# GitHub release. The route around it is feature branch -> dev -> a release/x.y branch cut from dev; +# main gets a merge only for a release. 4.0.0-beta.1 confirms that much of it - its lightweight tag +# sits on a merge into origin/release/4.0.0 and is on neither main nor dev - but not this script's +# own flow: that tag names a merge rather than an "add unitypackage" commit, and no beta.1 artifact +# is tracked under Releases/ at all. +# +# Three things here decide what actually ships, none of them obvious from the flags: +# +# - It REBUILDS the package unless --skip-build, so a run without that flag puts different bytes +# under the tag than whatever was accepted. Pass --skip-build to release the file you tested. +# - It creates its own commit, "add unitypackage ", and tags THAT. On a clean build run +# here, that commit's parent is the source it was built from, so the lineage is not lost. What +# the parent does not establish is the actual build inputs: nothing checks that the tree was +# clean, and with --skip-build the package may have been built from another commit entirely. +# Record the source SHA and build the artifact from a clean tree if that has to be provable. +# - It pushes HEAD and the tag without checking which branch it is on or whether the tree is +# clean. Standing on the wrong branch releases that branch. +# +# The tag is lightweight, which is why --notes-file is required to create a release: with +# --notes-from-tag GitHub falls back to the commit message and the notes read "add unitypackage +# ". --prerelease is off by default and has to be passed explicitly - to this script, and +# to any `gh release create` run by hand after --skip-github-release. set -euo pipefail @@ -16,6 +40,11 @@ PACKAGE_NAME="adapty-unity-plugin-$VERSION.unitypackage" ROOT_PACKAGE_PATH="$PROJECT_ROOT/$PACKAGE_NAME" RELEASE_PACKAGE_PATH="$RELEASES_DIR/$PACKAGE_NAME" +fail() { + echo "$1" >&2 + exit 1 +} + DRY_RUN=0 SKIP_BUILD=0 SKIP_COMMIT=0 @@ -24,6 +53,7 @@ SKIP_GITHUB_RELEASE=0 FORCE=0 DRAFT=0 PRERELEASE=0 +NOTES_FILE="" usage() { cat < Release notes. Required unless --skip-github-release: the tag this + script writes is lightweight, so GitHub would otherwise fall back + to the commit message. -h, --help Show this help message. Environment: @@ -78,6 +111,11 @@ while [[ $# -gt 0 ]]; do --prerelease) PRERELEASE=1 ;; + --notes-file) + [[ $# -ge 2 ]] || fail "--notes-file needs a path" + NOTES_FILE="$2" + shift + ;; -h|--help) usage exit 0 @@ -101,11 +139,6 @@ run() { fi } -fail() { - echo "$1" >&2 - exit 1 -} - if [[ -z "$VERSION" ]]; then fail "Package version not found in $PACKAGE_JSON" fi @@ -126,6 +159,13 @@ if [[ "$DRY_RUN" -eq 0 && "$SKIP_GITHUB_RELEASE" -eq 0 ]] && ! command -v gh >/d fail "GitHub CLI is required. Install/authenticate gh or pass --skip-github-release." fi +if [[ "$SKIP_GITHUB_RELEASE" -eq 0 ]]; then + # The tag written below is lightweight, so --notes-from-tag would make the release notes read + # "add unitypackage ". Refuse to publish rather than publish that. + [[ -n "$NOTES_FILE" ]] || fail "--notes-file is required to create a release. Pass it, or --skip-github-release to publish the release yourself." + [[ -f "$NOTES_FILE" ]] || fail "Notes file not found: $NOTES_FILE" +fi + if [[ -e "$RELEASE_PACKAGE_PATH" && "$FORCE" -eq 0 ]]; then fail "Release package already exists: $RELEASE_PACKAGE_PATH. Pass --force to replace it." fi @@ -156,7 +196,7 @@ else fi if [[ "$SKIP_GITHUB_RELEASE" -eq 0 ]]; then - GH_ARGS=(release create "$TAG" "$RELEASE_PACKAGE_PATH" --title "$TAG" --notes-from-tag) + GH_ARGS=(release create "$TAG" "$RELEASE_PACKAGE_PATH" --title "$TAG" --notes-file "$NOTES_FILE") if [[ "$DRAFT" -eq 1 ]]; then GH_ARGS+=(--draft) @@ -174,6 +214,10 @@ if [[ "$SKIP_GITHUB_RELEASE" -eq 0 ]]; then run gh "${GH_ARGS[@]}" fi else + MANUAL="gh release create \"$TAG\" \"$RELEASE_PACKAGE_PATH\" --title \"$TAG\" --notes-file " + [[ "$DRAFT" -eq 1 ]] && MANUAL="$MANUAL --draft" + [[ "$PRERELEASE" -eq 1 ]] && MANUAL="$MANUAL --prerelease" echo "Skipping GitHub Release. Upload manually with:" - echo "gh release create \"$TAG\" \"$RELEASE_PACKAGE_PATH\" --title \"$TAG\" --notes-from-tag" + echo "$MANUAL" + echo "Not --notes-from-tag: this tag is lightweight, so the notes become the commit message." fi diff --git a/tests/AdaptySDK.NextTests/AdaptySDK.NextTests.csproj b/tests/AdaptySDK.NextTests/AdaptySDK.NextTests.csproj new file mode 100644 index 0000000..83c0973 --- /dev/null +++ b/tests/AdaptySDK.NextTests/AdaptySDK.NextTests.csproj @@ -0,0 +1,70 @@ + + + + + + net8.0 + LatestMajor + 9 + disable + disable + false + AdaptySDK.NextTests + CS0169;CS0649;CS0414;CS0067 + false + $(MSBuildThisFileDirectory)../../Packages/com.adapty.unity-sdk/Runtime + $(MSBuildThisFileDirectory)../../Packages/com.adapty.unity-sdk/Editor + $(MSBuildThisFileDirectory)../shared + + + + + UNITY_EDITOR + + + + $(DefineConstants);$(AdaptyPlatform) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/AdaptySDK.NextTests/AotContractTests.cs b/tests/AdaptySDK.NextTests/AotContractTests.cs new file mode 100644 index 0000000..bf32af4 --- /dev/null +++ b/tests/AdaptySDK.NextTests/AotContractTests.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using AdaptySDK.Serialization; +using Newtonsoft.Json.Serialization; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + ///

+ /// No contract may need a collection wrapper, because the wrapper cannot be built on a + /// stripped IL2CPP player. + /// + /// + /// Stripping removes the wrapper's constructor, which Newtonsoft looks up by reflection, and + /// deserialization then fails outright. Desktop cannot reproduce that - nothing is stripped - + /// so what is asserted here is the property that holds on both: no wrapper is ever asked for. + /// + [TestFixture] + public class AotContractTests + { + [Test] + public void NoModelCollectionNeedsAWrapper() + { + var wrapped = CollectionTypes() + .Where(NeedsWrapper) + .Select(type => type.ToString()) + .Distinct() + .OrderBy(name => name, StringComparer.Ordinal) + .ToList(); + + Assert.That( + wrapped, + Is.Empty, + "these resolve to a wrapper Newtonsoft builds by reflection, " + + "which a stripped player cannot do:\n " + string.Join("\n ", wrapped) + ); + } + + /// + /// A guard that stops finding collections would pass no matter what the resolver did. + /// + [Test] + public void TheGuardStillFindsTheModelCollections() + { + var found = CollectionTypes().Distinct().ToList(); + + Assert.Multiple(() => + { + Assert.That(found.Count, Is.GreaterThan(5), "far fewer collections than the models declare"); + + Assert.That( + found, + Has.Some.EqualTo(typeof(List)), + "AdaptyProfile.AppliedAttributionSources is no longer seen" + ); + + // The models now hand out read-only views over concrete storage, so the interface + // case the wrapper rule exists for survives only where a member is still typed as + // one - today AdaptyFlowPaywall's internal product references. + Assert.That( + found, + Has.Some.EqualTo(typeof(IList)), + "no interface-typed collection is left for the wrapper rule to guard" + ); + Assert.That( + found, + Has.Some.EqualTo(typeof(Dictionary>)), + "AdaptyProfile.NonSubscriptions is no longer seen" + ); + }); + } + + private static bool NeedsWrapper(Type type) + { + var contract = AdaptyContractResolver.Instance.ResolveContract(type); + if (contract is not JsonArrayContract && contract is not JsonDictionaryContract) + { + return false; + } + + // Newtonsoft keeps the flag internal; read it rather than restate the rule, so the test + // tracks what the serializer will actually do. + var flag = contract + .GetType() + .GetProperty( + "ShouldCreateWrapper", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic + ); + + Assert.That(flag, Is.Not.Null, $"Newtonsoft no longer exposes ShouldCreateWrapper on {contract.GetType().Name}"); + + return (bool)flag.GetValue(contract); + } + + /// + /// Every collection the serializer contracts: a member of a model, or anything nested in + /// one. Only contract types are walked - the converters' own caches are dictionaries too, + /// and no serializer ever sees them. + /// + private static IEnumerable CollectionTypes() + { + const BindingFlags Members = + BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + + var models = typeof(Adapty) + .Assembly.GetTypes() + .Where(type => type.GetCustomAttribute() != null); + + foreach (var type in models) + { + var declared = type.GetFields(Members) + .Select(field => field.FieldType) + .Concat(type.GetProperties(Members).Select(property => property.PropertyType)); + + foreach (var member in declared) + { + foreach (var collection in Collections(member)) + { + yield return collection; + } + } + } + } + + /// + /// The type itself and its generic arguments, which the serializer contracts in turn: a + /// dictionary of lists asks for a wrapper twice. + /// + private static IEnumerable Collections(Type type) + { + if (type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type)) + { + yield return type; + } + + if (!type.IsGenericType) + { + yield break; + } + + foreach (var argument in type.GetGenericArguments()) + { + foreach (var nested in Collections(argument)) + { + yield return nested; + } + } + } + } +} diff --git a/tests/AdaptySDK.NextTests/CallbackTests.cs b/tests/AdaptySDK.NextTests/CallbackTests.cs new file mode 100644 index 0000000..d119ac8 --- /dev/null +++ b/tests/AdaptySDK.NextTests/CallbackTests.cs @@ -0,0 +1,111 @@ +using System; +using NUnit.Framework; +#if !UNITY_IOS && !UNITY_ANDROID +using AdaptySDK.Noop; +#endif + +namespace AdaptySDK.NextTests +{ + /// + /// The one policy behind every call back into the app. Requests own it in AdaptyRequest and + /// events go through the helper, so what it does is stated once here rather than implied by + /// each of the call sites it replaced. + /// + [TestFixture] + public class CallbackTests + { + [Test] + public void TheInvocationHappens() + { + var called = false; + + AdaptyCallbacks.InvokeSafe(() => called = true, "context"); + + Assert.That(called, Is.True); + } + + /// + /// Safe is not swallowed: the app's exception still reaches whoever asked for the call. What + /// the helper adds is the context — on a request that text is what the caller sees, and on + /// an event it is what OnMessage logs before containing it. + /// + [Test] + public void AThrowingCallbackKeepsItsContextAndItsCause() + { + var cause = new InvalidOperationException("the app's own bug"); + + Assert.That( + () => AdaptyCallbacks.InvokeSafe(() => throw cause, "Failed to invoke Something(..)"), + Throws + .InstanceOf() + .With.Message.EqualTo("Failed to invoke Something(..)") + .And.InnerException.SameAs(cause) + ); + } + + /// + /// Every call site guards a callback the app may not have supplied, and passes the + /// null-conditional in rather than a null delegate — so the lambda runs and does nothing. + /// + [Test] + public void AnAbsentCallbackIsNotAnError() + { + Action absent = null; + + Assert.That(() => AdaptyCallbacks.InvokeSafe(() => absent?.Invoke(1), "context"), Throws.Nothing); + } + +#if !UNITY_IOS && !UNITY_ANDROID + /// + /// The name in the diagnostic is the compiler's, not a copy. The two tests below are what + /// makes that true rather than merely intended: nothing else ties the text a request throws + /// to the method the app actually called. + /// + /// + /// They drive the no-op bridge, which answers synchronously, so the app's exception comes + /// back out of the public call itself. + /// + [TearDown] + public void ClearTheBridge() => AdaptyNoop.Handler = null; + + /// + /// A typed request. GetOnboarding is deliberately the subject: it is the one call + /// that used to hand the app's exception on raw, so this is the regression as well as the + /// guard. + /// + [Test] + public void ATypedRequestNamesTheMethodTheAppCalled() + { + AdaptyNoop.Handler = (method, request) => "{\"success\":null}"; + var cause = new InvalidOperationException("the app's own bug"); + + Assert.That( + () => Adapty.GetOnboarding("placement", (onboarding, error) => throw cause), + Throws + .InstanceOf() + .With.Message.EqualTo("Failed to invoke completionHandler in GetOnboarding(..)") + .And.InnerException.SameAs(cause) + ); + } + + /// + /// An error-only request, which reaches the transport through a second hop. The name has to + /// survive it — without the explicit hand-off the message would read SendVoid. + /// + [Test] + public void AnErrorOnlyRequestNamesTheMethodAndNotTheHelper() + { + AdaptyNoop.Handler = (method, request) => "{\"success\":true}"; + var cause = new InvalidOperationException("the app's own bug"); + + Assert.That( + () => Adapty.Logout(error => throw cause), + Throws + .InstanceOf() + .With.Message.EqualTo("Failed to invoke completionHandler in Logout(..)") + .And.InnerException.SameAs(cause) + ); + } +#endif + } +} diff --git a/tests/AdaptySDK.NextTests/ContractEnforcementTests.cs b/tests/AdaptySDK.NextTests/ContractEnforcementTests.cs new file mode 100644 index 0000000..2c284a6 --- /dev/null +++ b/tests/AdaptySDK.NextTests/ContractEnforcementTests.cs @@ -0,0 +1,705 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Runtime.Serialization; +using AdaptySDK.TestSupport; +using AdaptySDK.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using UnityEngine; + +namespace AdaptySDK.NextTests +{ + /// + /// The negative half of the contract: a payload that omits a required key has to fail, and so + /// does one carrying a value the contract does not list. + /// + /// + /// A green snapshot matrix cannot show either of these - both need a payload no fixture + /// contains. The required-key cases are generated from the fixtures themselves, so a field + /// that gains IsRequired later is covered without editing this file. + /// + [TestFixture] + public class ContractEnforcementTests + { + /// + /// Drops one required key at a time from a fixture and expects each removal to throw. + /// + [TestCase("profile-full", typeof(AdaptyProfile))] + [TestCase("profile-minimal", typeof(AdaptyProfile))] + [TestCase("flow-full", typeof(AdaptyFlow))] + [TestCase("flow-minimal", typeof(AdaptyFlow))] + [TestCase("onboarding-full", typeof(AdaptyOnboarding))] + [TestCase("onboarding-minimal", typeof(AdaptyOnboarding))] + [TestCase("purchase-result-success", typeof(AdaptyPurchaseResult))] + public void RequiredKeysAreEnforced(string fixture, System.Type type) + { + var json = Snapshots.LoadResponse(fixture); + var required = RequiredKeyPaths(JToken.Parse(json), type); + + Assert.That(required, Is.Not.Empty, "no required keys found - the walk is broken"); + + foreach (var path in required) + { + var mutilated = JToken.Parse(json); + mutilated.SelectToken(path).Parent.Remove(); + + Assert.That( + () => JsonConvert.DeserializeObject(mutilated.ToString(), type, Settings()), + Throws.InstanceOf(), + $"removing '{path}' was accepted" + ); + } + } + + [TestCase("products-full")] + public void RequiredKeysAreEnforcedInProducts(string fixture) + { + var json = Snapshots.LoadResponse(fixture); + var required = RequiredKeyPaths(JToken.Parse(json)[0], typeof(AdaptyPaywallProduct)); + + Assert.That(required, Is.Not.Empty); + + foreach (var path in required) + { + var mutilated = JToken.Parse(json); + mutilated.SelectToken(path).Parent.Remove(); + + Assert.That( + () => + JsonConvert.DeserializeObject>( + mutilated.ToString(), + Settings() + ), + Throws.InstanceOf(), + $"removing '{path}' was accepted" + ); + } + } + + /// + /// Values handled by a hand-written converter never reach the contract resolver, so their + /// required keys have to be listed rather than derived. + /// + [TestCase("{}", "element_type")] + [TestCase("{\"element_type\":\"select\"}", "value")] + [TestCase("{\"element_type\":\"select\",\"value\":{\"value\":\"f\",\"label\":\"F\"}}", "id")] + [TestCase("{\"element_type\":\"select\",\"value\":{\"id\":\"g\",\"label\":\"F\"}}", "value")] + [TestCase("{\"element_type\":\"select\",\"value\":{\"id\":\"g\",\"value\":\"f\"}}", "label")] + [TestCase("{\"element_type\":\"multi_select\",\"value\":{}}", "value")] + [TestCase("{\"element_type\":\"input\",\"value\":{}}", "type")] + [TestCase("{\"element_type\":\"input\",\"value\":{\"type\":\"text\"}}", "value")] + [TestCase("{\"element_type\":\"input\",\"value\":{\"type\":\"number\"}}", "value")] + [TestCase("{\"element_type\":\"date_picker\"}", "value")] + public void OnboardingStateRequiresKey(string json, string missing) => + Assert.That( + () => AdaptyJson.Deserialize(json), + Throws + .InstanceOf() + .With.Message.Contains($"'{missing}'") + ); + + [TestCase("{}", "name")] + public void AnalyticsEventRequiresKey(string json, string missing) => + Assert.That( + () => AdaptyJson.Deserialize(json), + Throws + .InstanceOf() + .With.Message.Contains($"'{missing}'") + ); + + [TestCase("{}", "status")] + [TestCase("{\"status\":\"determined\",\"details\":{\"app_launch_count\":1}}", "install_time")] + [TestCase( + "{\"status\":\"determined\",\"details\":{\"install_time\":\"2026-07-30T10:00:00.000Z\"}}", + "app_launch_count" + )] + public void InstallationStatusRequiresKey(string json, string missing) => + Assert.That( + () => AdaptyJson.Deserialize(json), + Throws + .InstanceOf() + .With.Message.Contains($"'{missing}'") + ); + + /// + /// The one key the contract requires on a branch rather than always, which no attribute can + /// state. A deserialization callback enforces it, and Newtonsoft invokes one through + /// MethodInfo.Invoke — so the model's complaint arrives wrapped. Both boundaries a + /// reply crosses catch Exception and print it, inner exception included. + /// + [Test] + public void DeterminedInstallationStatusRequiresDetails() => + Assert.That( + () => AdaptyJson.Deserialize("{\"status\":\"determined\"}"), + Throws + .InstanceOf() + .With.InnerException.InstanceOf() + .And.InnerException.Message.Contains("'details'") + ); + + /// + /// The other half of the same invariant: details belong to the determined branch, so a stray + /// one does not reach the app. The branch-per-subclass model this replaced never carried it + /// either. + /// + [TestCase("not_available")] + [TestCase("not_determined")] + public void InstallationDetailsOutsideTheDeterminedBranchAreDropped(string status) + { + var parsed = AdaptyJson.Deserialize( + "{\"status\":\"" + + status + + "\",\"details\":{\"install_time\":\"2026-07-30T10:00:00.000Z\"," + + "\"app_launch_count\":1}}" + ); + + Assert.That(parsed.Details, Is.Null); + } + + [TestCase("{\"phases\":[]}", "offer_identifier")] + [TestCase("{\"offer_identifier\":{\"id\":\"x\"}}", "type")] + [TestCase("{\"offer_identifier\":{\"id\":\"x\",\"type\":\"promotional\"}}", "phases")] + [TestCase("{\"offer_identifier\":{\"type\":\"promotional\"},\"phases\":[]}", "id")] + [TestCase("{\"offer_identifier\":{\"type\":\"win_back\"},\"phases\":[]}", "id")] +#if UNITY_ANDROID + // The contract marks the id required on the introductory branch for Android as well. + [TestCase("{\"offer_identifier\":{\"type\":\"introductory\"},\"phases\":[]}", "id")] +#endif + public void SubscriptionOfferRequiresKey(string json, string missing) => + Assert.That( + () => AdaptyJson.Deserialize(json), + Throws + .InstanceOf() + .With.Message.Contains($"'{missing}'") + ); + + /// + /// A string the contract does not list has to fail the read. + /// + /// + /// The SDK ships with the native SDKs it is pinned to, so an unlisted value is a broken + /// payload rather than one from the future. Where the contract does want an open set it + /// says so - a flow permission and an onboarding event name are strings, not enums - and + /// where it lists "unknown" itself the value is a member like any other, see + /// . + /// + [TestCase( + "{\"offer_identifier\":{\"id\":\"x\",\"type\":\"loyalty_reward\"},\"phases\":[]}", + typeof(AdaptySubscriptionOffer) + )] + [TestCase("{\"type\":\"teleport\"}", typeof(AdaptyUIUserAction))] + [TestCase("{\"type\":\"close\",\"open_in\":\"holodeck\"}", typeof(AdaptyUIUserAction))] + [TestCase("{\"type\":\"deferred_to_the_afterlife\"}", typeof(AdaptyPurchaseResult))] + // A near miss is not a value either: the C# member name, another casing of the contract + // value, and the value with whitespace around it are all outside the contract. + [TestCase("{\"type\":\"UserCancelled\"}", typeof(AdaptyPurchaseResult))] + [TestCase("{\"type\":\"Close\"}", typeof(AdaptyUIUserAction))] + [TestCase("{\"type\":\"USER_CANCELLED\"}", typeof(AdaptyPurchaseResult))] + [TestCase("{\"type\":\" user_cancelled \"}", typeof(AdaptyPurchaseResult))] + [TestCase("{\"type\":\"SystemBack\"}", typeof(AdaptyUIUserAction))] + public void UnknownEnumValueIsRejected(string json, System.Type type) => + Assert.That( + () => JsonConvert.DeserializeObject(json, type, Settings()), + Throws.InstanceOf() + ); + + /// + /// The two enums whose contract lists "unknown" among its values keep reading it. + /// + [Test] + public void ContractsOwnUnknownIsRead() + { + Assert.That( + AdaptyJson.Deserialize("\"unknown\""), + Is.EqualTo(AdaptyPaymentMode.Unknown) + ); + Assert.That( + AdaptyJson.Deserialize("\"unknown\""), + Is.EqualTo(AdaptySubscriptionPeriodUnit.Unknown) + ); + } + + /// + /// The other half of the same rule: outside the branches that require an offer id, one has + /// to keep reading without it. The introductory branch is in this half everywhere except + /// Android, where the contract requires the id too. + /// + [TestCase("code")] +#if !UNITY_ANDROID + [TestCase("introductory")] +#endif + public void OfferIdIsOptionalOutsideItsRequiredBranches(string type) + { + var offer = AdaptyJson.Deserialize( + "{\"offer_identifier\":{\"type\":\"" + type + "\"},\"phases\":[]}" + ); + + Assert.That(offer.Identifier, Is.Null); + } + + /// + /// Every member of a string enum carries exactly one contract name, and no two members share + /// it. + /// + /// + /// Asked of the metadata, because neither half of the converter can report it. Writing is + /// stock StringEnumConverter, which falls back to the C# member name instead of + /// failing, so a member that lost its [EnumMember] would quietly send "Unknown". + /// Reading is the ordinal map built from the same attributes, where a duplicate name means + /// one of the two members can never be read and which one is decided by field order. + /// + [Test] + public void EveryMemberOfAContractNamedEnumHasItsName() + { + var broken = new List(); + + foreach (var type in typeof(AdaptyFlow).Assembly.GetTypes()) + { + if (!type.IsEnum || type.Namespace != "AdaptySDK") + { + continue; + } + + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static); + + // No name anywhere means the contract spells this one as a number. + if (!System.Array.Exists(fields, HasContractName)) + { + continue; + } + + var seen = new Dictionary(); + + foreach (var field in fields) + { + var name = field.GetCustomAttribute()?.Value; + + if (string.IsNullOrEmpty(name)) + { + broken.Add($"{type.Name}.{field.Name} - no contract name"); + continue; + } + + if (seen.TryGetValue(name, out var owner)) + { + broken.Add($"{type.Name}.{field.Name} - shares \"{name}\" with {owner}"); + continue; + } + + seen[name] = field.Name; + } + } + + Assert.That( + broken, + Is.Empty, + "these break the mapping to the contract:\n " + string.Join("\n ", broken) + ); + } + + /// + /// The other half of the same rule: an enum the contract spells as a number must not be + /// caught by the string converter. Stock StringEnumConverter claims every enum, so + /// what keeps error codes numeric is the gate in front of it. + /// + [Test] + public void NumericEnumsStayNumeric() + { + Assert.That( + AdaptyJson.Serialize(AdaptyErrorCode.NoPurchasesToRestore), + Is.EqualTo("1004") + ); + Assert.That( + AdaptyJson.Deserialize("1004"), + Is.EqualTo(AdaptyErrorCode.NoPurchasesToRestore) + ); + Assert.That( + AdaptyJson.Serialize(AppTrackingTransparencyStatus.Authorized), + Is.EqualTo("3") + ); + Assert.That( + AdaptyJson.Deserialize("3"), + Is.EqualTo(AppTrackingTransparencyStatus.Authorized) + ); + } + + private static bool HasContractName(FieldInfo field) => + field.GetCustomAttribute() != null; + + /// + /// Every public enum member states its number. An inserted member otherwise renumbers every + /// member below it, and the numbers are public API even where the wire format is a string. + /// + /// + /// Read from the sources, because metadata cannot tell an explicit value from one the + /// compiler counted out. The approved public surface is what catches a number that moves; + /// this is what catches a number that was never written down. + /// + [Test] + public void EveryPublicEnumMemberStatesItsValue() + { + var models = System.IO.Path.Combine( + ProjectDirectory(), + "..", + "..", + "Packages", + "com.adapty.unity-sdk", + "Runtime", + "Models" + ); + + var implicitly_ = new List(); + + foreach (var file in System.IO.Directory.GetFiles(models, "*.cs")) + { + string enumeration = null; + var depth = 0; + + foreach (var line in System.IO.File.ReadAllLines(file)) + { + var declaration = Regex.Match(line, @"public enum (\w+)"); + if (declaration.Success) + { + enumeration = declaration.Groups[1].Value; + depth = 0; + } + + if (enumeration is null) + { + continue; + } + + depth += Count(line, '{') - Count(line, '}'); + + var member = Regex.Match(line, @"^[ \t]+([A-Za-z_]\w*)[ \t]*(,?)[ \t]*(//.*)?$"); + if (member.Success) + { + implicitly_.Add($"{enumeration}.{member.Groups[1].Value}"); + } + + if (depth == 0 && line.Contains("}")) + { + enumeration = null; + } + } + } + + Assert.That( + implicitly_, + Is.Empty, + "these take their number from the member above them:\n " + + string.Join("\n ", implicitly_) + ); + } + + private static int Count(string line, char character) + { + var found = 0; + foreach (var c in line) + { + if (c == character) + { + found += 1; + } + } + return found; + } + + private static string ProjectDirectory( + [System.Runtime.CompilerServices.CallerFilePath] string callerPath = null + ) => System.IO.Path.GetDirectoryName(callerPath); + + /// + /// A response model hands out views, not its own storage. Declaring the member as a + /// read-only interface would not be enough on its own: ReadOnlyCollection and + /// ReadOnlyDictionary do implement the mutable interfaces, so the cast back compiles + /// and succeeds — what it yields is the wrapper, which refuses to write, rather than the + /// dictionary behind it. + /// + [Test] + public void AResponseModelCannotBeMutatedThroughItsCollections() + { + var profile = AdaptyJson.Deserialize( + Snapshots.LoadResponse("profile-full") + ); + + Assert.Multiple(() => + { + Assert.That( + () => ((IDictionary)profile.AccessLevels).Clear(), + Throws.InstanceOf() + ); + Assert.That( + () => ((IDictionary)profile.Subscriptions).Clear(), + Throws.InstanceOf() + ); + Assert.That( + () => ((IDictionary)profile.CustomAttributes)["x"] = 1, + Throws.InstanceOf() + ); + Assert.That( + () => ((IList)profile.AppliedAttributionSources).Add("x"), + Throws.InstanceOf() + ); + + // Both levels: the values of the outer dictionary are views too. + foreach (var purchases in profile.NonSubscriptions.Values) + { + Assert.That( + () => ((IList)purchases).Clear(), + Throws.InstanceOf() + ); + } + }); + + Assert.That(profile.NonSubscriptions.Values, Is.Not.Empty, "the fixture stopped covering the nested case"); + + Assert.That( + () => + ((IDictionary>)profile.NonSubscriptions).Clear(), + Throws.InstanceOf(), + "the outer dictionary is writable" + ); + } + + /// + /// The mirror on the way in: what the SDK will send has to be decided when the setter is + /// called, not whenever the request happens to be serialized. Nothing else would catch this + /// — the public surface and the happy-path snapshots look the same either way. + /// + [Test] + public void AParameterObjectDoesNotKeepTheCallersDictionary() + { + var tags = new Dictionary { ["greeting"] = "hello" }; + var parameters = new AdaptyUICreateFlowViewParameters().SetCustomTags(tags); + + tags["greeting"] = "goodbye"; + tags["added_later"] = "x"; + + Assert.Multiple(() => + { + Assert.That(parameters.CustomTags["greeting"], Is.EqualTo("hello")); + Assert.That(parameters.CustomTags.ContainsKey("added_later"), Is.False); + Assert.That(AdaptyJson.Serialize(parameters), Does.Not.Contain("added_later")); + }); + } + + /// + /// The same ownership question for the one asset built from a caller's buffer. + /// + [Test] + public void ACustomAssetDoesNotKeepTheCallersBuffer() + { + var pixels = new byte[] { 1, 2, 3 }; + var asset = (AdaptyCustomAssetLocalImageData)AdaptyCustomAsset.LocalImageData(pixels); + + pixels[0] = 99; + asset.Data[1] = 99; + + Assert.That(AdaptyJson.Serialize(asset), Does.Contain(Convert.ToBase64String(new byte[] { 1, 2, 3 }))); + } + + /// + /// A Gradient is the family's other mutable argument, and the payload is read from it + /// lazily, so the window a caller can change it in runs until the request goes out. + /// + [Test] + public void ACustomAssetDoesNotKeepTheCallersGradient() + { + var gradient = new Gradient + { + colorKeys = new[] { new GradientColorKey(new Color(1f, 0f, 0f, 1f), 0f) }, + alphaKeys = new[] { new GradientAlphaKey(1f, 0f) }, + }; + var asset = (AdaptyCustomAssetLinearGradient)AdaptyCustomAsset.LinearGradient(gradient); + + gradient.colorKeys = new[] { new GradientColorKey(new Color(0f, 1f, 0f, 1f), 0f) }; + + Assert.That(AdaptyJson.Serialize(asset), Does.Contain("#FF0000FF")); + } + + /// + /// An unknown discriminator is forward compatibility; a missing one is a broken payload. + /// The two must not be confused. + /// + [Test] + public void UnknownDiscriminatorIsNotAMissingOne() + { + Assert.That( + AdaptyJson.Deserialize( + "{\"element_type\":\"slider\",\"value\":{}}" + ), + Is.Null + ); + + var analytics = AdaptyJson.Deserialize( + "{\"name\":\"screen_dismissed\"}" + ); + Assert.That(analytics, Is.TypeOf()); + } + + /// + /// The builder's description is for logs and the debugger, and its format is deliberately + /// not pinned - but a member absent from it is one a developer cannot see while diagnosing + /// a configuration, although it reaches the request like all the others. Adding a field and + /// forgetting the description has already happened once. + /// + [Test] + public void TheConfigurationBuilderDescribesEveryMemberItCarries() + { + var description = new AdaptyConfiguration.Builder("key_under_test").ToString(); + + var undescribed = new List(); + foreach ( + var field in typeof(AdaptyConfiguration.Builder).GetFields( + BindingFlags.Instance | BindingFlags.Public + ) + ) + { + if (!description.Contains(field.Name)) + { + undescribed.Add(field.Name); + } + } + + Assert.That( + undescribed, + Is.Empty, + "these are carried into the configuration but missing from Builder.ToString():\n " + + string.Join("\n ", undescribed) + ); + } + + private static JsonSerializerSettings Settings() + { + var settings = new JsonSerializerSettings(); + var serializer = AdaptyJson.CreateSerializer(); + + settings.ContractResolver = serializer.ContractResolver; + settings.NullValueHandling = serializer.NullValueHandling; + settings.MissingMemberHandling = serializer.MissingMemberHandling; + settings.DateParseHandling = serializer.DateParseHandling; + settings.FloatParseHandling = serializer.FloatParseHandling; + settings.Culture = serializer.Culture; + settings.ConstructorHandling = serializer.ConstructorHandling; + settings.ObjectCreationHandling = serializer.ObjectCreationHandling; + settings.MetadataPropertyHandling = serializer.MetadataPropertyHandling; + foreach (var converter in serializer.Converters) + { + settings.Converters.Add(converter); + } + return settings; + } + + /// + /// Walks the contract of alongside the fixture and returns the + /// JSONPath of every value the contract marks required. + /// + /// + /// The paths come from rather than being assembled here: + /// subscriptions are keyed by vendor product id, and a hand-built path would read the dots + /// in "com.adapty.sample.monthly" as separators. + /// + private static List RequiredKeyPaths(JToken node, System.Type type) + { + var paths = new List(); + Walk(node, type, paths, 0); + return paths; + } + + private static void Walk(JToken node, System.Type type, List paths, int depth) + { + if (depth > 8) + { + return; + } + + if (node is JArray array) + { + var element = ElementType(type); + if (element is null) + { + return; + } + + foreach (var item in array) + { + Walk(item, element, paths, depth + 1); + } + return; + } + + if (node is not JObject map) + { + return; + } + + // A converter reads its payload by hand, so the resolver knows nothing about what is + // inside it and the walk would stop at the converter's type. Cross the boundary + // explicitly, so the annotated models nested under one stay covered. + if (type == typeof(AdaptySubscriptionOffer)) + { + if (map["offer_identifier"] is JToken identity) + { + paths.Add(identity.Path); + if (identity["type"] != null) + { + paths.Add(identity["type"].Path); + } + } + + Walk(map["phases"], typeof(IList), paths, depth + 1); + return; + } + + var resolved = AdaptyContractResolver.Instance.ResolveContract(type); + + // Access levels and subscriptions arrive keyed by identifier, so the models inside + // them are only reachable through the dictionary's value type. + if (resolved is Newtonsoft.Json.Serialization.JsonDictionaryContract dictionary) + { + foreach (var entry in map) + { + Walk(entry.Value, dictionary.DictionaryValueType, paths, depth + 1); + } + return; + } + + if (resolved is not Newtonsoft.Json.Serialization.JsonObjectContract contract) + { + return; + } + + foreach (var property in contract.Properties) + { + var child = map[property.PropertyName]; + if (child is null) + { + continue; + } + + if (property.Required == Required.Always) + { + paths.Add(child.Path); + } + + Walk(child, property.PropertyType, paths, depth + 1); + } + } + + private static System.Type ElementType(System.Type type) + { + if (type.IsArray) + { + return type.GetElementType(); + } + + return type.IsGenericType && type.GetGenericArguments().Length == 1 + ? type.GetGenericArguments()[0] + : null; + } + } +} diff --git a/tests/AdaptySDK.NextTests/DependencyPlanTests.cs b/tests/AdaptySDK.NextTests/DependencyPlanTests.cs new file mode 100644 index 0000000..7395119 --- /dev/null +++ b/tests/AdaptySDK.NextTests/DependencyPlanTests.cs @@ -0,0 +1,103 @@ +using System.Linq; +using AdaptySDK.Editor; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// What Adapty SDK > Install Dependencies decides to install, from what the project + /// already has. + /// + /// + /// The v3 to v4 upgrade is the case worth pinning: v3 declared External Dependency Manager + /// 1.2.187, v4 needs 1.2.188, and the difference is invisible until an iOS build resolves + /// against the wrong Xcode project path. Presence alone used to be the whole test, so an + /// upgraded project was told it had everything. + /// + [TestFixture] + public class DependencyPlanTests + { + private const string Edm = AdaptyDependencyPlan.EdmId + "@" + AdaptyDependencyPlan.EdmVersion; + private const string Newtonsoft = + AdaptyDependencyPlan.NewtonsoftId + "@" + AdaptyDependencyPlan.NewtonsoftVersion; + + [Test] + public void AnAbsentDependencyManagerIsInstalled() => + Assert.That(Plan(AdaptyEdmSource.None), Is.EqualTo(new[] { Edm })); + + [TestCase("1.2.187", TestName = "the version v3 declared")] + [TestCase("1.2.0", TestName = "older still")] + [TestCase("1.1.999", TestName = "an older minor")] + public void AnOlderDependencyManagerIsUpgraded(string installed) => + Assert.That(Plan(AdaptyEdmSource.Package, installed), Is.EqualTo(new[] { Edm })); + + [TestCase("1.2.188", TestName = "exactly what the SDK asks for")] + [TestCase("1.2.189", TestName = "newer")] + [TestCase("1.3.0", TestName = "a newer minor")] + public void ADependencyManagerNewEnoughIsLeftAlone(string installed) + { + Assert.That(Plan(AdaptyEdmSource.Package, installed), Is.Empty); + Assert.That(AdaptyDependencyPlan.EdmCaution(AdaptyEdmSource.Package, installed), Is.Null); + } + + /// + /// Google ships its own .unitypackage under Assets/, where Package Manager + /// describes nothing. Installing the package over it would leave two copies, so the plan + /// says so instead of acting. + /// + [Test] + public void ADependencyManagerOutsidePackageManagerIsReportedRatherThanReplaced() + { + Assert.That(Plan(AdaptyEdmSource.Unmanaged), Is.Empty); + + Assert.That( + AdaptyDependencyPlan.EdmCaution(AdaptyEdmSource.Unmanaged, null), + Does.Contain(AdaptyDependencyPlan.EdmVersion).And.Contain("cannot be read") + ); + } + + /// + /// A version string that will not parse is not evidence of anything, and least of all a + /// reason to install over whatever is there. + /// + [TestCase("")] + [TestCase("1.2.188-preview.1")] + [TestCase("latest")] + public void AVersionThatCannotBeComparedIsReported(string installed) + { + Assert.That(Plan(AdaptyEdmSource.Package, installed), Is.Empty); + Assert.That( + AdaptyDependencyPlan.EdmCaution(AdaptyEdmSource.Package, installed), + Does.Contain(AdaptyDependencyPlan.EdmVersion) + ); + } + + [Test] + public void NewtonsoftIsInstalledWhenTheProjectDoesNotHaveIt() => + Assert.That( + AdaptyDependencyPlan.Missing(false, AdaptyEdmSource.Package, "1.2.188"), + Is.EqualTo(new[] { Newtonsoft }) + ); + + [Test] + public void BothAreInstalledWhenTheProjectHasNeither() => + Assert.That( + AdaptyDependencyPlan.Missing(false, AdaptyEdmSource.None, null), + Is.EqualTo(new[] { Newtonsoft, Edm }) + ); + + /// + /// Newtonsoft carries no version check of its own: the SDK assembly is gated on the package + /// being there, and any version of it that Package Manager resolves makes the SDK compile. + /// + [Test] + public void APresentNewtonsoftIsLeftAtWhateverVersionItIs() => + Assert.That( + AdaptyDependencyPlan.Missing(true, AdaptyEdmSource.Package, "1.2.188"), + Is.Empty + ); + + private static string[] Plan(AdaptyEdmSource edm, string version = null) => + AdaptyDependencyPlan.Missing(true, edm, version).ToArray(); + } +} diff --git a/tests/AdaptySDK.NextTests/DocumentationTests.cs b/tests/AdaptySDK.NextTests/DocumentationTests.cs new file mode 100644 index 0000000..a6d96d3 --- /dev/null +++ b/tests/AdaptySDK.NextTests/DocumentationTests.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Xml.Linq; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// Every public type and member of the SDK has to be documented, because IntelliSense is where + /// a caller reads the contract — not the converter, the YAML or the native SDK. + /// + /// + /// The compiler already knows: `CS1591` names every undocumented public member, and the + /// surface project turns the documentation file on. It is a warning rather than an error + /// because two things share that assembly and are not the SDK's surface — the Unity stubs the + /// suites link in, and the deprecated tree, which by policy is maintained rather than brought + /// in line. So the check reads the generated file and applies the same exclusions. + /// + [TestFixture] + public class DocumentationTests + { + [Test] + public void EveryPublicMemberIsDocumented() + { + var summarised = Summarised(); + + using var context = Open(out var package); + + var missing = new List(); + + foreach (var type in package.GetTypes().Where(IsSurface)) + { + var name = type.FullName.Replace('+', '.'); + + if (!summarised.Contains("T:" + name)) + { + missing.Add(name); + } + + var members = Members(type).ToList(); + var overloads = members + .GroupBy(member => member.Name, StringComparer.Ordinal) + .ToDictionary(group => group.Key, group => group.Count(), StringComparer.Ordinal); + + foreach (var member in members.GroupBy(member => member.Name).Select(group => group.First())) + { + if (!IsSummarised(summarised, name, member, overloads[member.Name])) + { + missing.Add($"{name}.{member.Name}"); + } + } + } + + Assert.That( + missing.OrderBy(name => name, StringComparer.Ordinal).ToList(), + Is.Empty, + "these are public and carry no XML documentation:\n " + string.Join("\n ", missing) + ); + } + + /// + /// A check that stops finding the surface would pass whatever the sources said. + /// + [Test] + public void TheCheckStillSeesTheSurface() + { + using var context = Open(out var package); + + var types = package.GetTypes().Where(IsSurface).ToList(); + + Assert.Multiple(() => + { + Assert.That(types.Count, Is.GreaterThan(40), "far fewer public types than the SDK has"); + Assert.That( + types.SelectMany(Members).Count(), + Is.GreaterThan(150), + "the member rule no longer matches what it is meant to cover" + ); + Assert.That(Summarised().Count, Is.GreaterThan(250), "the documentation file is not being read"); + }); + } + + /// + /// The SDK's own public types. The Unity stubs stand in for types Unity ships, and the + /// deprecated tree is exempt by the policy in AGENTS.md. + /// + private static bool IsSurface(Type type) => + (type.IsPublic || (type.IsNestedPublic && IsSurface(type.DeclaringType))) + && type.Namespace != null + && type.Namespace.StartsWith("AdaptySDK", StringComparison.Ordinal) + && !type.GetCustomAttributesData().Any(data => + data.AttributeType.FullName == "System.ObsoleteAttribute" + ); + + private static IEnumerable Members(Type type) + { + const BindingFlags Declared = + BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly; + + foreach (var member in type.GetMembers(Declared)) + { + if (member is MethodInfo method && (method.IsSpecialName || method.DeclaringType == typeof(object))) + { + continue; + } + + // A nested type is walked as a type of its own, and the file names it `T:`. + if (member is TypeInfo) + { + continue; + } + + if (member is FieldInfo field && field.Name == "value__") + { + continue; + } + + // A parameterless constructor is usually the implicit one, which has no + // declaration to document and which CS1591 does not report either. What such a + // type needs said is said on the type. + if (member is ConstructorInfo constructor && constructor.GetParameters().Length == 0) + { + continue; + } + + if (member.GetCustomAttributesData().Any(data => + data.AttributeType.FullName == "System.ObsoleteAttribute")) + { + continue; + } + + yield return member; + } + } + + /// + /// Every member the file gives a summary for. An entry is not enough on its own — + /// a lone param or remarks produces one, and CS1591 is satisfied by it too, + /// so neither would notice a member that says nothing about itself. + /// + private static HashSet Summarised() + { + var entries = Entries(); + + var duplicated = entries + .Where(entry => entry.Elements("summary").Count() > 1) + .Select(entry => entry.Attribute("name").Value) + .ToList(); + + Assert.That( + duplicated, + Is.Empty, + "these carry more than one summary, so only the first is shown:\n " + + string.Join("\n ", duplicated) + ); + + return new HashSet( + entries + .Where(entry => entry.Elements("summary").Any(summary => + !string.IsNullOrWhiteSpace(summary.Value) + )) + .Select(entry => entry.Attribute("name").Value), + StringComparer.Ordinal + ); + } + + private static List Entries() + { + var file = Path.Combine(Output(), "AdaptySDK.Surface.xml"); + + Assert.That( + File.Exists(file), + Is.True, + "the surface project stopped emitting its documentation file, so this check would pass on nothing" + ); + + return XDocument.Load(file).Descendants("member").ToList(); + } + + /// + /// Whether the file gives this member a summary. A field, property or event is named + /// exactly; a method carries its parameter list, so it is matched by its name and the + /// bracket that follows — which is what stops one member standing in for another whose + /// name merely begins the same way, the failure a plain prefix match allows. + /// + /// + /// Overloads are counted rather than matched one by one: rendering the parameter types the + /// way the compiler writes them is a lot of machinery for the same answer. Two overloads + /// and one summary between them fails, which is the case that matters. + /// + private static bool IsSummarised( + HashSet summarised, + string type, + MemberInfo member, + int overloads + ) + { + var name = member.Name == ".ctor" ? "#ctor" : member.Name; + var stem = $"{type}.{name}"; + + if (member is MethodBase) + { + var documented = summarised.Count(entry => + entry == "M:" + stem + || entry.StartsWith("M:" + stem + "(", StringComparison.Ordinal) + ); + + return documented >= overloads; + } + + return summarised.Contains("F:" + stem) + || summarised.Contains("P:" + stem) + || summarised.Contains("E:" + stem); + } + + private static MetadataLoadContext Open(out Assembly package) + { + var assemblies = Directory + .GetFiles(Output(), "*.dll") + .Concat(Directory.GetFiles(AppContext.BaseDirectory, "*.dll")) + .Concat(Directory.GetFiles(Path.GetDirectoryName(typeof(object).Assembly.Location), "*.dll")) + .GroupBy(Path.GetFileName) + .Select(group => group.First()) + .ToList(); + + var context = new MetadataLoadContext(new PathAssemblyResolver(assemblies)); + package = context.LoadFromAssemblyPath(Path.Combine(Output(), "AdaptySDK.Surface.dll")); + return context; + } + + private static string Output() => + Path.Combine( + Path.GetDirectoryName(SourcePath()), + "..", + "surface", + "package", + "bin", + "Debug", + "net8.0" + ); + + private static string SourcePath([CallerFilePath] string path = null) => path; + } +} diff --git a/tests/AdaptySDK.NextTests/EventDispatchTests.cs b/tests/AdaptySDK.NextTests/EventDispatchTests.cs new file mode 100644 index 0000000..3c804ee --- /dev/null +++ b/tests/AdaptySDK.NextTests/EventDispatchTests.cs @@ -0,0 +1,418 @@ +// The bridge is chosen by the same #if the SDK uses: off the editor it is a real P/Invoke or +// AndroidJavaClass with nothing behind it on a desktop test host. These fixtures drive the +// transport end to end, so they need the no-op bridge; device coverage is a separate stage. +// The platform-dependent payloads themselves - the custom asset paths - are pinned per platform +// by the request snapshots. +#if !UNITY_IOS && !UNITY_ANDROID + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using AdaptySDK.Noop; +using AdaptySDK.TestSupport; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// Events arrive from native code. On iOS that is a reverse-P/Invoke callback with nothing + /// behind it, so the dispatcher's first duty is to never let anything escape - and its second + /// is to hand the listener a fully built model. + /// + [TestFixture] + public class EventDispatchTests + { + private Listener _listener; + + [SetUp] + public void Setup() + { + _listener = new Listener(); + Adapty.SetEventListener(_listener); + } + + [TearDown] + public void TearDown() => Adapty.SetEventListener(null); + + [Test] + public void ProfileUpdatesReachTheListener() + { + Adapty.OnMessage( + "did_load_latest_profile", + "{\"profile\":" + Snapshots.LoadResponse("profile-minimal") + "}" + ); + + Assert.That(_listener.Profile, Is.Not.Null); + Assert.That(_listener.Profile.ProfileId, Is.Not.Null); + } + + /// + /// The wire is UTC and the public API is local, on this path too: an event is a document + /// parsed before anything typed is built out of it. + /// + [Test] + public void ProfileDatesReachTheListenerAsLocalTime() + { + Adapty.OnMessage( + "did_load_latest_profile", + "{\"profile\":" + Snapshots.LoadResponse("profile-full") + "}" + ); + + Assert.That(_listener.Profile, Is.Not.Null); + + var premium = _listener.Profile.AccessLevels["premium"]; + + Assert.Multiple(() => + { + Assert.That( + premium.ActivatedAt.Kind, + Is.EqualTo(DateTimeKind.Local), + "a date reached the listener in the wrong zone" + ); + Assert.That( + premium.ActivatedAt.ToUniversalTime(), + Is.EqualTo(new DateTime(2026, 1, 15, 9, 30, 0, DateTimeKind.Utc)), + "the zone was right and the instant was not" + ); + }); + } + + /// + /// Every way a payload can be wrong, none of which may reach the caller as an exception. + /// + [TestCase("not json at all", TestName = "malformed json")] + [TestCase("[]", TestName = "not an object")] + [TestCase("{}", TestName = "missing payload")] + [TestCase("{\"profile\":null}", TestName = "null payload")] + [TestCase("{\"profile\":{}}", TestName = "payload missing required fields")] + [TestCase("{\"profile\":\"a string\"}", TestName = "payload of the wrong shape")] + public void BrokenPayloadsAreContained(string json) + { + Assert.That(() => Adapty.OnMessage("did_load_latest_profile", json), Throws.Nothing); + Assert.That(_listener.Profile, Is.Null, "a broken payload reached the listener"); + } + + /// + /// The listener is the app's code, and it throwing is the app's bug - but it happens on the + /// same callback, so it cannot be allowed to take the process down either. + /// + [Test] + public void AThrowingListenerIsContained() + { + _listener.Throw = true; + + Assert.That( + () => + Adapty.OnMessage( + "did_load_latest_profile", + "{\"profile\":" + Snapshots.LoadResponse("profile-minimal") + "}" + ), + Throws.Nothing + ); + } + + [Test] + public void UnknownEventIdsAreIgnored() => + Assert.That( + () => Adapty.OnMessage("something_from_a_newer_native_sdk", "{}"), + Throws.Nothing + ); + + [TestCase(null)] + [TestCase("")] + public void EmptyPayloadsAreIgnored(string json) => + Assert.That(() => Adapty.OnMessage("did_load_latest_profile", json), Throws.Nothing); + + /// + /// The analytic event's params are the third payload the contract leaves untyped, and the + /// only one that reaches the app through the dispatcher rather than through a model. It has + /// to arrive as the CLR graph of doubles that 3.x handed over, not as Newtonsoft's own + /// shapes. + /// + [Test] + public void AnalyticEventParamsArriveAsALooseGraph() + { + var flows = new FlowsListener(); + Adapty.SetFlowsEventsListener(flows); + + try + { + Adapty.OnMessage( + "flow_view_did_receive_analytic_event", + "{\"view\":{\"id\":\"v\",\"placement_id\":\"p\",\"variation_id\":\"var\"}," + + "\"name\":\"purchase_started\"," + + "\"params\":{\"count\":7,\"nested\":{\"k\":1},\"list\":[1,2]," + + "\"released_at\":\"2026-07-30T10:00:00.000Z\"}}" + ); + + Assert.That(flows.Params, Is.Not.Null, "the event never reached the listener"); + Assert.Multiple(() => + { + Assert.That(flows.Params["count"], Is.EqualTo(7d).And.TypeOf()); + Assert.That(flows.Params["nested"], Is.TypeOf>()); + Assert.That(flows.Params["list"], Is.TypeOf>()); + + // An untyped payload is not the place to recognise dates: the app gets back + // what was sent, character for character. + Assert.That( + flows.Params["released_at"], + Is.EqualTo("2026-07-30T10:00:00.000Z").And.TypeOf() + ); + }); + } + finally + { + Adapty.SetFlowsEventsListener(null); + } + } + + /// + /// respond is a delegate the SDK hands to app code, and the app may invoke it from + /// any thread - an OS permission callback rarely arrives on the main one. The answer it + /// produces is a request, and on Android the bridge is JNI, which a thread Unity did not + /// attach cannot enter - so the send has to reach the bridge from the main thread, whatever + /// thread the app answered on. + /// + [Test] + public void RespondFromABackgroundThreadReachesTheBridgeOnTheMainThread() + { + var pump = new PumpingContext(); + var previous = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(pump); + + try + { + Adapty.InitializeTransport(); + + string sentMethod = null; + string sentRequest = null; + var sentOnThread = -1; + AdaptyNoop.Handler = (method, request) => + { + sentMethod = method; + sentRequest = request; + sentOnThread = Thread.CurrentThread.ManagedThreadId; + return "{\"success\":true}"; + }; + + var handler = new PermissionHandler(); + Adapty.SetSystemRequestsHandler(handler); + + Adapty.OnMessage( + "flow_view_did_ask_permission", + "{\"view\":{\"id\":\"v\",\"placement_id\":\"p\",\"variation_id\":\"var\"}," + + "\"event_id\":\"evt-1\",\"permission\":\"camera\"}" + ); + + Assert.That(handler.Respond, Is.Not.Null, "the request never reached the handler"); + + var worker = new Thread(() => handler.Respond(true, "os said yes")); + worker.Start(); + worker.Join(); + + Assert.That(sentMethod, Is.Null, "the answer reached the bridge from the worker thread"); + + pump.RunAll(); + + Assert.Multiple(() => + { + Assert.That(sentMethod, Is.EqualTo("flow_view_did_answer_permission")); + Assert.That(sentRequest, Does.Contain("\"status\":\"granted\"")); + Assert.That(sentRequest, Does.Contain("\"detail\":\"os said yes\"")); + Assert.That(sentOnThread, Is.EqualTo(Thread.CurrentThread.ManagedThreadId)); + }); + } + finally + { + Adapty.SetSystemRequestsHandler(null); + AdaptyNoop.Handler = null; + SynchronizationContext.SetSynchronizationContext(previous); + + // Re-capture, so the pump this test made does not stay the SDK's main thread. + Adapty.InitializeTransport(); + } + } + + /// + /// The observer-mode reports are the other delegates handed to app code, and a billing + /// implementation answers on its own threads. Same rule, same route. + /// + [Test] + public void AnObserverReportFromABackgroundThreadReachesTheBridgeOnTheMainThread() + { + var pump = new PumpingContext(); + var previous = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(pump); + + try + { + Adapty.InitializeTransport(); + + string sentMethod = null; + var sentOnThread = -1; + AdaptyNoop.Handler = (method, request) => + { + sentMethod = method; + sentOnThread = Thread.CurrentThread.ManagedThreadId; + return "{\"success\":true}"; + }; + + var resolver = new Resolver(); + Adapty.SetObserverModeResolver(resolver); + + Adapty.OnMessage( + "flow_view_observer_did_initiate_restore", + "{\"view\":{\"id\":\"v\",\"placement_id\":\"p\",\"variation_id\":\"var\"}," + + "\"event_id\":\"evt-2\"}" + ); + + Assert.That(resolver.StartRestore, Is.Not.Null, "the event never reached the resolver"); + + var worker = new Thread(() => resolver.StartRestore()); + worker.Start(); + worker.Join(); + + Assert.That(sentMethod, Is.Null, "the report reached the bridge from the worker thread"); + + pump.RunAll(); + + Assert.Multiple(() => + { + Assert.That(sentMethod, Is.EqualTo("observer_restore_did_start")); + Assert.That(sentOnThread, Is.EqualTo(Thread.CurrentThread.ManagedThreadId)); + }); + } + finally + { + Adapty.SetObserverModeResolver(null); + AdaptyNoop.Handler = null; + SynchronizationContext.SetSynchronizationContext(previous); + Adapty.InitializeTransport(); + } + } + + private sealed class PumpingContext : SynchronizationContext + { + private readonly ConcurrentQueue> _queue = + new ConcurrentQueue>(); + + public override void Post(SendOrPostCallback d, object state) => + _queue.Enqueue(new KeyValuePair(d, state)); + + internal void RunAll() + { + while (_queue.TryDequeue(out var work)) + { + work.Key(work.Value); + } + } + } + + private sealed class PermissionHandler : IAdaptyUISystemRequestsHandler + { + internal Action Respond; + + public void FlowViewDidAskPermission( + AdaptyUIFlowView view, + string permission, + IReadOnlyDictionary customArgs, + Action respond + ) => Respond = respond; + + public void FlowViewDidRequestAppReview(AdaptyUIFlowView view) { } + } + + private sealed class Resolver : IAdaptyUIObserverModeResolver + { + internal Action StartRestore; + + public void FlowViewDidInitiatePurchase( + AdaptyUIFlowView view, + AdaptyPaywallProduct product, + Action onStartPurchase, + Action onFinishPurchase + ) { } + + public void FlowViewDidInitiateRestore( + AdaptyUIFlowView view, + Action onStartRestore, + Action onFinishRestore + ) => StartRestore = onStartRestore; + } + + private sealed class FlowsListener : IAdaptyFlowsEventsListener + { + internal IReadOnlyDictionary Params; + + public void FlowViewDidReceiveAnalyticEvent( + AdaptyUIFlowView view, + string name, + IReadOnlyDictionary @params + ) => Params = @params; + + public void FlowViewDidAppear(AdaptyUIFlowView view) { } + + public void FlowViewDidDisappear(AdaptyUIFlowView view) { } + + public void FlowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIUserAction action) { } + + public void FlowViewDidSelectProduct(AdaptyUIFlowView view, string productId) { } + + public void FlowViewDidStartPurchase( + AdaptyUIFlowView view, + AdaptyPaywallProduct product + ) { } + + public void FlowViewDidFinishPurchase( + AdaptyUIFlowView view, + AdaptyPaywallProduct product, + AdaptyPurchaseResult purchasedResult + ) { } + + public void FlowViewDidFailPurchase( + AdaptyUIFlowView view, + AdaptyPaywallProduct product, + AdaptyError error + ) { } + + public void FlowViewDidStartRestore(AdaptyUIFlowView view) { } + + public void FlowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { } + + public void FlowViewDidFailRestore(AdaptyUIFlowView view, AdaptyError error) { } + + public void FlowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) { } + + public void FlowViewDidFailLoadingProducts(AdaptyUIFlowView view, AdaptyError error) { } + + public void FlowViewDidFinishWebPaymentNavigation( + AdaptyUIFlowView view, + AdaptyPaywallProduct product, + AdaptyError error + ) { } + } + + private sealed class Listener : IAdaptyEventListener + { + internal AdaptyProfile Profile; + internal bool Throw; + + public void OnLoadLatestProfile(AdaptyProfile profile) + { + if (Throw) + { + throw new InvalidOperationException("the app's own bug"); + } + + Profile = profile; + } + + public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { } + + public void OnInstallationDetailsFail(AdaptyError error) { } + } + } +} + +#endif diff --git a/tests/AdaptySDK.NextTests/KidsModeTraitTests.cs b/tests/AdaptySDK.NextTests/KidsModeTraitTests.cs new file mode 100644 index 0000000..f054842 --- /dev/null +++ b/tests/AdaptySDK.NextTests/KidsModeTraitTests.cs @@ -0,0 +1,138 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using AdaptySDK.Editor; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// The edit that decides whether a Kids Category build ships IDFA. It is text surgery on + /// project.pbxproj, so the only meaningful evidence is a project Unity and External Dependency + /// Manager really produced — a hand-written sample would test the shape this suite imagines. + /// + /// + /// The fixtures are a contiguous excerpt of one: lines 2865-2956 of the Unity-iPhone project + /// this repository generates today - Unity 6000.4.5f1, External Dependency Manager 1.2.188, + /// AdaptySDK-iOS pinned at 4.0.2 - from the XCConfigurationList section through the end of the + /// package sections. .applied.pbxproj is that build untouched, trait and all; + /// edm-package-reference.pbxproj is what External Dependency Manager wrote, recovered by + /// deleting the three lines the postprocessor had inserted. Round-tripping between them is what + /// pins the format down to the tab. + /// + /// Regenerate both by building for iOS with ADAPTY_KIDS_MODE and taking the same excerpt; the + /// object ids differ every build, so no test may name one. + /// + [TestFixture] + public class KidsModeTraitTests + { + private const string Section = "/* Begin XCRemoteSwiftPackageReference section */"; + + [Test] + public void TheTraitLandsExactlyWhereTheRealBuildPutIt() + { + Assert.That( + AdaptyIOSKidsModeTrait.Enable(Fixture("edm-package-reference.pbxproj")), + Is.EqualTo(Fixture("edm-package-reference.applied.pbxproj")) + ); + } + + /// + /// The postprocessor runs on every build, including the ones after the first. + /// + [Test] + public void ASecondPassChangesNothing() + { + var applied = Fixture("edm-package-reference.applied.pbxproj"); + + Assert.That(AdaptyIOSKidsModeTrait.Enable(applied), Is.EqualTo(applied)); + } + + /// + /// The reference is found by its isa, not by the brace nearest the URL. Left to the + /// brace alone, a URL appearing anywhere else in the project would aim the insertion at + /// whatever object happened to enclose it — and the build would report Kids Mode while + /// still linking IDFA, which nothing downstream would catch. + /// + [Test] + public void AUrlOutsideAPackageReferenceIsNotMistakenForOne() + { + // An object that mentions the URL and is not a package reference. It sits before the + // real one, so a search that stops at the first occurrence stops here. + const string Decoy = + "\t\tAAAA1111 /* PBXBuildFile */ = {\n" + + "\t\t\tisa = PBXBuildFile;\n" + + "\t\t\tcomment = \"see https://github.com/adaptyteam/AdaptySDK-iOS.git\";\n" + + "\t\t};\n" + + Section; + + Assert.That( + AdaptyIOSKidsModeTrait.Enable( + Fixture("edm-package-reference.pbxproj").Replace(Section, Decoy) + ), + Is.EqualTo(Fixture("edm-package-reference.applied.pbxproj").Replace(Section, Decoy)) + ); + } + + [Test] + public void AProjectWithoutTheReferenceFails() + { + var project = Fixture("edm-package-reference.pbxproj") + .Replace(AdaptyIOSKidsModeTrait.PackageUrl, "someoneelse/OtherSDK"); + + Assert.That( + () => AdaptyIOSKidsModeTrait.Enable(project), + Throws.InvalidOperationException.With.Message.Contains("no AdaptySDK-iOS") + ); + } + + /// + /// Two references are as unsafe as none: the trait would go on whichever came first. + /// + [Test] + public void TwoReferencesFailRatherThanPickOne() + { + // Located by the section markers rather than by the object's id, which is generated + // afresh on every build. + var project = Fixture("edm-package-reference.pbxproj"); + var open = project.IndexOf(Section, StringComparison.Ordinal) + Section.Length; + var close = project.IndexOf("/* End XCRemoteSwiftPackageReference", StringComparison.Ordinal); + var reference = project.Substring(open, close - open); + + Assert.That( + () => AdaptyIOSKidsModeTrait.Enable(project.Replace(reference, reference + reference)), + Throws.InvalidOperationException.With.Message.Contains("2 AdaptySDK-iOS") + ); + } + + /// + /// A traits block that is not ours is not merged into — it is reported, because guessing + /// the merge is how a trait someone added by hand gets dropped. + /// + [Test] + public void AForeignTraitsBlockIsReported() + { + var project = Fixture("edm-package-reference.applied.pbxproj") + .Replace(AdaptyIOSKidsModeTrait.Trait, "SomeOtherTrait"); + + Assert.That( + () => AdaptyIOSKidsModeTrait.Enable(project), + Throws.InvalidOperationException.With.Message.Contains("already declares a traits block") + ); + } + + private static string Fixture(string name) => + File.ReadAllText( + Path.Combine( + Path.GetDirectoryName(SourcePath()), + "..", + "shared", + "Fixtures", + "pbxproj", + name + ) + ); + + private static string SourcePath([CallerFilePath] string path = null) => path; + } +} diff --git a/tests/AdaptySDK.NextTests/LegacyOnboardingDispatchTests.cs b/tests/AdaptySDK.NextTests/LegacyOnboardingDispatchTests.cs new file mode 100644 index 0000000..c71f162 --- /dev/null +++ b/tests/AdaptySDK.NextTests/LegacyOnboardingDispatchTests.cs @@ -0,0 +1,256 @@ +// Same bridge constraint as EventDispatchTests: the transport needs the no-op bridge. +#if !UNITY_IOS && !UNITY_ANDROID + +using System; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// The legacy onboarding events are dispatched from a method of their own, split out of the + /// main switch so that its deprecation warnings stay in one place. These pin what that move has + /// to preserve: every id reaching its own listener method with a built model, and nothing + /// escaping on the way out. + /// + [TestFixture] + [Obsolete("Covers the legacy onboarding API, which is deprecated in favor of Flows.")] + public class LegacyOnboardingDispatchTests + { + private const string View = + "\"view\":{\"id\":\"view-1\",\"placement_id\":\"placement-1\",\"variation_id\":\"variation-1\"}"; + private const string Meta = + "\"meta\":{\"onboarding_id\":\"onboarding-1\",\"screen_cid\":\"screen-1\",\"screen_index\":2,\"total_screens\":5}"; + + private Listener _listener; + + [SetUp] + public void Setup() + { + _listener = new Listener(); + Adapty.SetOnboardingsEventsListener(_listener); + } + + [TearDown] + public void TearDown() => Adapty.SetOnboardingsEventsListener(null); + + [Test] + public void DidFinishLoadingCarriesTheViewAndTheMeta() + { + Adapty.OnMessage("onboarding_did_finish_loading", "{" + View + "," + Meta + "}"); + + Assert.That(_listener.Called, Is.EqualTo("did_finish_loading")); + Assert.That(_listener.View.Id, Is.EqualTo("view-1")); + Assert.That(_listener.View.PlacementId, Is.EqualTo("placement-1")); + Assert.That(_listener.Meta.OnboardingId, Is.EqualTo("onboarding-1")); + Assert.That(_listener.Meta.ScreenIndex, Is.EqualTo(2)); + Assert.That(_listener.Meta.ScreensTotal, Is.EqualTo(5)); + } + + [Test] + public void DidFailWithErrorCarriesTheError() + { + Adapty.OnMessage( + "onboarding_did_fail_with_error", + "{" + View + ",\"error\":{\"adapty_code\":1004,\"message\":\"No purchases\"}}" + ); + + Assert.That(_listener.Called, Is.EqualTo("did_fail_with_error")); + Assert.That(_listener.View.Id, Is.EqualTo("view-1")); + Assert.That(_listener.Error.Code, Is.EqualTo(AdaptyErrorCode.NoPurchasesToRestore)); + Assert.That(_listener.Error.Message, Is.EqualTo("No purchases")); + } + + /// + /// Three ids that share a payload shape and differ only in the method they must reach - + /// the case the move is most likely to get wrong. + /// + [TestCase("onboarding_on_close_action", "close_action")] + [TestCase("onboarding_on_paywall_action", "paywall_action")] + [TestCase("onboarding_on_custom_action", "custom_action")] + public void ActionEventsReachTheirOwnMethod(string id, string expected) + { + Adapty.OnMessage(id, "{" + View + "," + Meta + ",\"action_id\":\"act-1\"}"); + + Assert.That(_listener.Called, Is.EqualTo(expected)); + Assert.That(_listener.ActionId, Is.EqualTo("act-1")); + Assert.That(_listener.Meta.ScreenClientId, Is.EqualTo("screen-1")); + } + + [Test] + public void AnalyticsEventsArriveTyped() + { + Adapty.OnMessage( + "onboarding_on_analytics_action", + "{" + View + "," + Meta + ",\"event\":{\"name\":\"onboarding_started\"}}" + ); + + Assert.That(_listener.Called, Is.EqualTo("analytics_event")); + Assert.That( + _listener.AnalyticsEvent, + Is.TypeOf() + ); + } + + /// + /// The one case that reads the same object twice - the element id off the raw action, the + /// params through the converter. + /// + [Test] + public void StateUpdatedCarriesTheElementIdAndTheParams() + { + Adapty.OnMessage( + "onboarding_on_state_updated_action", + "{" + + View + + "," + + Meta + + ",\"action\":{\"element_id\":\"plan\",\"element_type\":\"select\"," + + "\"value\":{\"id\":\"plan-1\",\"value\":\"monthly\",\"label\":\"Monthly\"}}}" + ); + + Assert.That(_listener.Called, Is.EqualTo("state_updated")); + Assert.That(_listener.ElementId, Is.EqualTo("plan")); + Assert.That(_listener.Params, Is.TypeOf()); + } + + [Test] + public void AThrowingListenerIsContained() + { + _listener.Throw = true; + + Assert.That( + () => + Adapty.OnMessage( + "onboarding_did_finish_loading", + "{" + View + "," + Meta + "}" + ), + Throws.Nothing + ); + } + + [TestCase("{}", TestName = "missing view and meta")] + [TestCase("{\"view\":{}}", TestName = "view missing required fields")] + [TestCase("not json at all", TestName = "malformed json")] + public void BrokenPayloadsAreContained(string json) + { + Assert.That( + () => Adapty.OnMessage("onboarding_did_finish_loading", json), + Throws.Nothing + ); + Assert.That(_listener.Called, Is.Null, "a broken payload reached the listener"); + } + + [Test] + public void EventsWithoutAListenerAreIgnored() + { + Adapty.SetOnboardingsEventsListener(null); + + Assert.That( + () => + Adapty.OnMessage( + "onboarding_did_finish_loading", + "{" + View + "," + Meta + "}" + ), + Throws.Nothing + ); + } + + private sealed class Listener : IAdaptyOnboardingsEventsListener + { + internal string Called; + internal AdaptyUIOnboardingView View; + internal AdaptyUIOnboardingMeta Meta; + internal AdaptyError Error; + internal string ActionId; + internal string ElementId; + internal AdaptyOnboardingsStateUpdatedParams Params; + internal AdaptyOnboardingsAnalyticsEvent AnalyticsEvent; + internal bool Throw; + + private void Record(string called, AdaptyUIOnboardingView view) + { + Called = called; + View = view; + if (Throw) + throw new InvalidOperationException("the app's own bug"); + } + + public void OnboardingViewDidFailWithError( + AdaptyUIOnboardingView view, + AdaptyError error + ) + { + Error = error; + Record("did_fail_with_error", view); + } + + public void OnboardingViewDidFinishLoading( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta + ) + { + Meta = meta; + Record("did_finish_loading", view); + } + + public void OnboardingViewOnCloseAction( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta, + string actionId + ) + { + Meta = meta; + ActionId = actionId; + Record("close_action", view); + } + + public void OnboardingViewOnPaywallAction( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta, + string actionId + ) + { + Meta = meta; + ActionId = actionId; + Record("paywall_action", view); + } + + public void OnboardingViewOnCustomAction( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta, + string actionId + ) + { + Meta = meta; + ActionId = actionId; + Record("custom_action", view); + } + + public void OnboardingViewOnStateUpdatedAction( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta, + string elementId, + AdaptyOnboardingsStateUpdatedParams @params + ) + { + Meta = meta; + ElementId = elementId; + Params = @params; + Record("state_updated", view); + } + + public void OnboardingViewOnAnalyticsEvent( + AdaptyUIOnboardingView view, + AdaptyUIOnboardingMeta meta, + AdaptyOnboardingsAnalyticsEvent analyticsEvent + ) + { + Meta = meta; + AnalyticsEvent = analyticsEvent; + Record("analytics_event", view); + } + } + } +} + +#endif diff --git a/tests/AdaptySDK.NextTests/ManifestTests.cs b/tests/AdaptySDK.NextTests/ManifestTests.cs new file mode 100644 index 0000000..050a34e --- /dev/null +++ b/tests/AdaptySDK.NextTests/ManifestTests.cs @@ -0,0 +1,132 @@ +using System.Text.Json; +using AdaptySDK.Editor; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// The OpenUPM registry is written into the user's Packages/manifest.json by hand, since + /// scoped registries have no public Package Manager API. A malformed edit breaks Package + /// Manager for the whole project, so every shape is parsed back with a strict parser - + /// System.Text.Json rather than Newtonsoft, which accepts trailing commas. + /// + public class ManifestTests + { + private const string Url = "https://package.openupm.com"; + private const string Scope = "com.google"; + + [Test] + public void AddsTheSectionWhenThereIsNone() + { + var result = AdaptyManifest.AddRegistry( + "{\n \"dependencies\": {\n \"com.unity.ugui\": \"2.0.0\"\n }\n}\n" + ); + + var root = Parsed(result); + Assert.That(Registries(root).GetArrayLength(), Is.EqualTo(1)); + Assert.That(Scopes(root, 0), Does.Contain(Scope)); + Assert.That(root.TryGetProperty("dependencies", out _), Is.True); + } + + [Test] + public void AddsTheSectionToAnEmptyManifest() + { + var root = Parsed(AdaptyManifest.AddRegistry("{}")); + + Assert.That(Registries(root).GetArrayLength(), Is.EqualTo(1)); + Assert.That(Scopes(root, 0), Does.Contain(Scope)); + } + + [Test] + public void AddsTheEntryToAnEmptyRegistryArray() + { + var root = Parsed( + AdaptyManifest.AddRegistry( + "{\n \"scopedRegistries\": [],\n \"dependencies\": {}\n}\n" + ) + ); + + Assert.That(Registries(root).GetArrayLength(), Is.EqualTo(1)); + Assert.That(Scopes(root, 0), Does.Contain(Scope)); + } + + [Test] + public void KeepsRegistriesThatAreAlreadyThere() + { + var root = Parsed( + AdaptyManifest.AddRegistry( + "{\n \"scopedRegistries\": [\n {\n \"name\": \"other\",\n" + + " \"url\": \"https://other.example\",\n \"scopes\": [\n" + + " \"com.other\"\n ]\n }\n ],\n \"dependencies\": {}\n}\n" + ) + ); + + Assert.That(Registries(root).GetArrayLength(), Is.EqualTo(2)); + Assert.That( + Registries(root)[1].GetProperty("url").GetString(), + Is.EqualTo("https://other.example") + ); + } + + [Test] + public void AddsTheScopeToAnEmptyScopeArray() + { + var root = Parsed(AdaptyManifest.AddRegistry(Registry("[]"))); + + Assert.That(Scopes(root, 0), Is.EqualTo(new[] { Scope })); + } + + [Test] + public void KeepsScopesThatAreAlreadyThere() + { + var root = Parsed( + AdaptyManifest.AddRegistry(Registry("[\n \"com.cysharp\"\n ]")) + ); + + Assert.That(Scopes(root, 0), Is.EquivalentTo(new[] { Scope, "com.cysharp" })); + } + + [Test] + public void LeavesAManifestThatAlreadyHasEverything() + { + var manifest = Registry($"[\n \"{Scope}\"\n ]"); + + Assert.That(AdaptyManifest.AddRegistry(manifest), Is.EqualTo(manifest)); + } + + [TestCase("")] + [TestCase(null)] + [TestCase("not json at all")] + [TestCase("{\n \"scopedRegistries\": \"not an array\"\n}")] + public void RefusesToEditWhatItCannotParse(string manifest) + { + Assert.That(AdaptyManifest.AddRegistry(manifest), Is.Null); + } + + private static string Registry(string scopes) => + "{\n \"scopedRegistries\": [\n {\n \"name\": \"package.openupm.com\",\n" + + $" \"url\": \"{Url}\",\n \"scopes\": {scopes}\n }}\n ],\n" + + " \"dependencies\": {}\n}\n"; + + private static JsonElement Parsed(string manifest) + { + Assert.That(manifest, Is.Not.Null, "the edit was refused"); + return JsonDocument.Parse(manifest).RootElement; + } + + private static JsonElement Registries(JsonElement root) => + root.GetProperty("scopedRegistries"); + + private static string[] Scopes(JsonElement root, int index) + { + var scopes = Registries(root)[index].GetProperty("scopes"); + var result = new string[scopes.GetArrayLength()]; + for (var i = 0; i < result.Length; i++) + { + result[i] = scopes[i].GetString(); + } + + return result; + } + } +} diff --git a/tests/AdaptySDK.NextTests/PackageManifestTests.cs b/tests/AdaptySDK.NextTests/PackageManifestTests.cs new file mode 100644 index 0000000..7f0bc2d --- /dev/null +++ b/tests/AdaptySDK.NextTests/PackageManifestTests.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// package.json is what a release is cut from — the tag, the artifact name and what + /// Package Manager resolves all come from it. Two other places restate parts of it: the + /// version the SDK reports at runtime, and the dependency versions the installer asks Package + /// Manager for on behalf of .unitypackage users. + /// + /// + /// Neither restatement is reachable from the other, so a bump that misses one produces a + /// release that builds, ships and is wrong: a tag and a filename carrying the new version + /// around an SDK that reports the old one. The release script derives everything from the + /// manifest and so cannot notice. + /// + [TestFixture] + public class PackageManifestTests + { + [Test] + public void TheVersionTheSdkReportsIsTheVersionItShipsAs() => + Assert.That(Adapty.SDKVersion, Is.EqualTo((string)Manifest()["version"])); + + /// + /// The installer is the only route to these packages for a .unitypackage project, + /// which has no manifest of its own for Package Manager to read. + /// + [Test] + public void TheInstallerAsksForEveryDependencyTheManifestDeclares() + { + var declared = Declared(); + + Assert.That(declared, Is.Not.Empty, "no dependencies read from package.json"); + Assert.That(Installed(), Is.EquivalentTo(declared)); + } + + private static Dictionary Declared() + { + var manifest = Manifest(); + + return new[] { "dependencies", "peerDependencies" } + .Select(section => manifest[section]) + .Where(section => section != null) + .SelectMany(section => ((JObject)section).Properties()) + .ToDictionary(property => property.Name, property => (string)property.Value); + } + + /// + /// The installer names each package in a pair of constants — <name>Id and + /// <name>Version — so the pairs are read back by that shared prefix rather + /// than by a list kept here, which would be a third copy of the same thing. + /// + private static Dictionary Installed() + { + var source = File.ReadAllText(PackageFile("Editor/AdaptyDependencyPlan.cs")); + + var constants = Regex + .Matches(source, @"const\s+string\s+(?\w+?)(?Id|Version)\s*=\s*""(?[^""]*)""") + .Cast() + .ToLookup(match => match.Groups["name"].Value); + + return constants + .Where(pair => pair.Count() == 2) + .ToDictionary( + pair => pair.Single(match => match.Groups["kind"].Value == "Id").Groups["value"].Value, + pair => pair.Single(match => match.Groups["kind"].Value == "Version").Groups["value"].Value + ); + } + + private static JObject Manifest() => JObject.Parse(File.ReadAllText(PackageFile("package.json"))); + + private static string PackageFile(string path) => + Path.Combine( + Path.GetDirectoryName(SourcePath()), + "..", + "..", + "Packages", + "com.adapty.unity-sdk", + path + ); + + private static string SourcePath([CallerFilePath] string path = null) => path; + } +} diff --git a/tests/AdaptySDK.NextTests/ParityTests.cs b/tests/AdaptySDK.NextTests/ParityTests.cs new file mode 100644 index 0000000..b07933c --- /dev/null +++ b/tests/AdaptySDK.NextTests/ParityTests.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using AdaptySDK; +using AdaptySDK.TestSupport; +using AdaptySDK.Serialization; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// Parses the fixtures and compares the full state of each model with its approved snapshot. + /// The snapshots were taken from the manual layer, so a diff is a change in what the SDK reads. + /// + [TestFixture] + public class ParityTests + { + /// + /// The contract puts profile in the success branch only, so the other two results + /// carry a null one. ToString has to survive that: an app logging a cancelled purchase is + /// not on an error path. + /// + [TestCase("purchase-result-success")] + [TestCase("purchase-result-pending")] + [TestCase("purchase-result-cancelled")] + public void PurchaseResultDescribesEveryVariant(string fixture) => + Assert.That( + () => + AdaptyJson + .Deserialize(Snapshots.LoadResponse(fixture)) + .ToString(), + Throws.Nothing + ); + + [TestCase("profile-full")] + [TestCase("profile-minimal")] + public void Profile(string fixture) => + Snapshots.Matches( + fixture, + ModelSnapshot.Render(AdaptyJson.Deserialize(Snapshots.LoadResponse(fixture))) + ); + + [TestCase("flow-full")] + [TestCase("flow-minimal")] + public void Flow(string fixture) => + Snapshots.Matches( + fixture, + ModelSnapshot.Render(AdaptyJson.Deserialize(Snapshots.LoadResponse(fixture))) + ); + + [TestCase("onboarding-full")] + [TestCase("onboarding-minimal")] + public void Onboarding(string fixture) => + Snapshots.Matches( + fixture, + ModelSnapshot.Render( + AdaptyJson.Deserialize(Snapshots.LoadResponse(fixture)) + ) + ); + + [TestCase("onboarding-analytics-started")] + [TestCase("onboarding-analytics-screen-completed")] + [TestCase("onboarding-analytics-screen-completed-bare")] + [TestCase("onboarding-analytics-unknown")] + public void OnboardingAnalyticsEvent(string fixture) + { + var payload = Newtonsoft.Json.Linq.JObject.Parse(Snapshots.LoadResponse(fixture)); + var analyticsEvent = payload["event"].ToObject( + AdaptyJson.CreateSerializer() + ); + Snapshots.Matches(fixture, ModelSnapshot.Render(analyticsEvent)); + } + + [TestCase("installation-determined")] + [TestCase("installation-determined-minimal")] + [TestCase("installation-not-determined")] + [TestCase("installation-not-available")] + public void InstallationStatus(string fixture) => + Snapshots.Matches( + fixture, + ModelSnapshot.Render( + AdaptyJson.Deserialize(Snapshots.LoadResponse(fixture)) + ) + ); + + [TestCase("error-full")] + [TestCase("error-minimal")] + public void Error(string fixture) + { + var payload = Newtonsoft.Json.Linq.JObject.Parse(Snapshots.LoadResponse(fixture)); + var error = payload["error"].ToObject(AdaptyJson.CreateSerializer()); + Snapshots.Matches(fixture, ModelSnapshot.Render(error)); + } + + [TestCase("user-action-full")] + [TestCase("user-action-minimal")] + public void UserAction(string fixture) + { + var payload = Newtonsoft.Json.Linq.JObject.Parse(Snapshots.LoadResponse(fixture)); + var action = payload["action"].ToObject(AdaptyJson.CreateSerializer()); + Snapshots.Matches(fixture, ModelSnapshot.Render(action)); + } + + [TestCase("flow-full")] + public void RemoteConfigDictionary(string fixture) + { + var flow = AdaptyJson.Deserialize(Snapshots.LoadResponse(fixture)); + Snapshots.Matches( + fixture + "-remote-config-dictionary", + ModelSnapshot.Render(flow.RemoteConfig.Dictionary) + ); + } + + [TestCase("products-full")] + public void PaywallProducts(string fixture) => + Snapshots.Matches( + fixture, + ModelSnapshot.Render( + AdaptyJson.Deserialize>(Snapshots.LoadResponse(fixture)) + ) + ); + + [TestCase("purchase-result-success")] + [TestCase("purchase-result-pending")] + [TestCase("purchase-result-cancelled")] + public void PurchaseResult(string fixture) => + Snapshots.Matches( + fixture, + ModelSnapshot.Render( + AdaptyJson.Deserialize(Snapshots.LoadResponse(fixture)) + ) + ); + + [TestCase("onboarding-date-picker-full")] + [TestCase("onboarding-date-picker-partial")] + [TestCase("onboarding-select")] + public void OnboardingStateUpdated(string fixture) + { + var payload = Newtonsoft.Json.Linq.JObject.Parse(Snapshots.LoadResponse(fixture)); + var parameters = payload["action"].ToObject( + AdaptyJson.CreateSerializer() + ); + Snapshots.Matches(fixture, ModelSnapshot.Render(parameters)); + } + } +} diff --git a/tests/AdaptySDK.NextTests/PlayModeStateTests.cs b/tests/AdaptySDK.NextTests/PlayModeStateTests.cs new file mode 100644 index 0000000..93561e2 --- /dev/null +++ b/tests/AdaptySDK.NextTests/PlayModeStateTests.cs @@ -0,0 +1,135 @@ +#if !UNITY_IOS && !UNITY_ANDROID + +using System.Linq; +using System.Reflection; +using AdaptySDK.TestSupport; +using NUnit.Framework; +using UnityEngine; + +namespace AdaptySDK.NextTests +{ + /// + /// With Domain Reload disabled — the default for fast iteration — statics survive leaving Play + /// Mode. Anything the SDK holds that a developer registered has to be gone before the next run, + /// or that run receives the previous one's callbacks. + /// + /// + /// The reset is what Unity calls; the suites call it directly, which is the same thing minus + /// the Editor. What cannot be checked here is that Unity calls it at all — that is the two + /// consecutive Play Mode runs in the acceptance pass. + /// + [TestFixture] + public class PlayModeStateTests + { + [TearDown] + public void TearDown() => Adapty.SetEventListener(null); + + [Test] + public void AListenerDoesNotSurviveIntoTheNextRun() + { + Adapty.SetEventListener(new Listener()); + + Adapty.ResetListeners(); + + Adapty.OnMessage( + "did_load_latest_profile", + "{\"profile\":" + Snapshots.LoadResponse("profile-minimal") + "}" + ); + + Assert.That(Listener.Calls, Is.Zero, "a listener from the previous run was still called"); + } + + [Test] + public void TheNoopHandlerDoesNotSurviveEither() + { + AdaptySDK.Noop.AdaptyNoop.Handler = (method, request) => "{}"; + + AdaptySDK.Noop.AdaptyNoop.ResetHandler(); + + Assert.That(AdaptySDK.Noop.AdaptyNoop.Handler, Is.Null); + } + + /// + /// Every reset has to be one Unity actually calls, and it only calls the ones carrying the + /// attribute. A method renamed or added without it would leave its state behind silently — + /// and so would one registered for a later moment, since `AfterSceneLoad` runs once a scene + /// has had the chance to hand the SDK a listener. + /// + [Test] + public void EveryResetIsRegisteredWithUnity() + { + var resets = typeof(Adapty) + .Assembly.GetTypes() + .SelectMany(type => + type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly) + ) + .Where(method => method.Name.StartsWith("Reset")) + .ToList(); + + var unregistered = resets + .Where(method => + method.GetCustomAttribute()?.LoadType + != RuntimeInitializeLoadType.SubsystemRegistration + ) + .Select(method => $"{method.DeclaringType.Name}.{method.Name}") + .ToList(); + + Assert.Multiple(() => + { + Assert.That(resets, Is.Not.Empty, "no reset methods found - the rule matches nothing"); + Assert.That( + unregistered, + Is.Empty, + "these reset static state but are not registered for SubsystemRegistration:\n " + + string.Join("\n ", unregistered) + ); + }); + } + + /// + /// A different rule riding the same mechanism: this one registers the platform callback + /// transport rather than clearing anything. It is the only place that does, so the stage is + /// the whole of the guarantee - it has to precede the first scene, because a MonoBehaviour's + /// `Awake` is where an app calls the SDK. `AfterSceneLoad`, which is what the attribute + /// means with no argument at all, runs after that, and on a device every completion handler + /// would then go uncalled. + /// + [Test] + public void TheTransportIsRegisteredBeforeTheFirstScene() + { + var hook = typeof(Adapty).GetMethod( + nameof(Adapty.InitializeTransport), + BindingFlags.Static | BindingFlags.NonPublic + ); + + Assert.That(hook, Is.Not.Null, "Adapty.InitializeTransport is gone"); + + var attribute = hook.GetCustomAttribute(); + + Assert.Multiple(() => + { + Assert.That(attribute, Is.Not.Null, "Unity only calls it while it carries the attribute"); + Assert.That( + attribute?.LoadType, + Is.Not.EqualTo(RuntimeInitializeLoadType.AfterSceneLoad), + "the first scene has already had its chance to call the SDK by then" + ); + }); + } + + private sealed class Listener : IAdaptyEventListener + { + internal static int Calls; + + public Listener() => Calls = 0; + + public void OnLoadLatestProfile(AdaptyProfile profile) => Calls += 1; + + public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { } + + public void OnInstallationDetailsFail(AdaptyError error) { } + } + } +} + +#endif diff --git a/tests/AdaptySDK.NextTests/PublicSurfaceTests.cs b/tests/AdaptySDK.NextTests/PublicSurfaceTests.cs new file mode 100644 index 0000000..642ce5f --- /dev/null +++ b/tests/AdaptySDK.NextTests/PublicSurfaceTests.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using AdaptySDK.TestSupport; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// Pins the package's public API, member by member, against an approved snapshot: the behaviour + /// snapshots only cover what the tests happen to call. Read for metadata only, since this + /// project compiles the same type names into itself. + /// + [TestFixture] + public class PublicSurfaceTests + { + [Test] + public void ThePublicSurfaceIsUnchanged() + { + using var context = Open(out var package); + + var surface = string.Join( + Environment.NewLine, + Describe(package, "AdaptySDK").OrderBy(member => member, StringComparer.Ordinal) + ); + + Snapshots.Matches("public-surface", surface); + } + + /// + /// Renders every public member as one line, so the comparison is a set difference and the + /// snapshot reads as a diff. + /// + private static HashSet Describe(Assembly assembly, string root) + { + var members = new HashSet(StringComparer.Ordinal); + + foreach (var type in assembly.GetTypes().OrderBy(t => t.FullName, StringComparer.Ordinal)) + { + if (!IsVisible(type) || !type.Namespace.StartsWith(root, StringComparison.Ordinal)) + { + continue; + } + + members.Add( + $"{Access(type)}{Abstractness(type)}{Kind(type)} {type.FullName}{Bases(type)}" + ); + + const BindingFlags flags = + BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.Instance + | BindingFlags.Static + | BindingFlags.DeclaredOnly; + + foreach (var member in type.GetMembers(flags)) + { + var line = DescribeMember(type, member); + if (line != null) + { + members.Add(line); + } + } + } + + return members; + } + + private static string DescribeMember(Type type, MemberInfo member) + { + switch (member) + { + case FieldInfo field when IsVisible(field): + // An enum's members are its API, and so are their numeric values - a + // renumbering is a silent breaking change for anything that persisted one. + // The backing value__ field is not a literal and carries nothing. + var value = field.IsLiteral ? $" = {field.GetRawConstantValue()}" : string.Empty; + return $"{Access(field)}{Static(field.IsStatic)}{Mutability(field)}" + + $"{type.FullName}.{field.Name} : {Name(field.FieldType)}{value}"; + + case PropertyInfo property when IsVisible(property.GetMethod) + || IsVisible(property.SetMethod): + return $"{Access(property)}{Static(IsStatic(property))}" + + $"{type.FullName}.{property.Name} : {Name(property.PropertyType)}" + + $" {{{Accessor("get", property.GetMethod)}{Accessor("set", property.SetMethod)} }}"; + + case MethodInfo method when IsVisible(method) && !IsAccessor(type, method): + return $"{Access(method)}{Static(method.IsStatic)}{Inheritance(method)}" + + $"{type.FullName}.{method.Name}{Arity(method)}({Parameters(method)})" + + $" : {Name(method.ReturnType)}"; + + case ConstructorInfo constructor when IsVisible(constructor): + return $"{Access(constructor)}{type.FullName}.ctor({Parameters(constructor)})"; + + case EventInfo declared when IsVisible(declared.AddMethod): + return $"{Access(declared.AddMethod)}{Static(declared.AddMethod.IsStatic)}" + + $"{type.FullName}.{declared.Name} (event) : {Name(declared.EventHandlerType)}"; + + default: + return null; + } + } + + private static string Access(Type type) => + type.IsPublic || type.IsNestedPublic ? "public " + : type.IsNestedFamORAssem ? "protected internal " + : "protected "; + + private static string Access(FieldInfo field) => + field.IsPublic ? "public " + : field.IsFamilyOrAssembly ? "protected internal " + : "protected "; + + private static string Access(MethodBase method) => + method.IsPublic ? "public " + : method.IsFamilyOrAssembly ? "protected internal " + : "protected "; + + /// + /// A property's own accessibility is the widest of its accessors. + /// + private static string Access(PropertyInfo property) + { + var accessors = new[] { property.GetMethod, property.SetMethod } + .Where(a => a != null) + .ToList(); + + return accessors.Any(a => a.IsPublic) ? "public " + : accessors.Any(a => a.IsFamilyOrAssembly) ? "protected internal " + : "protected "; + } + + private static bool IsVisible(FieldInfo field) => + field.IsPublic || field.IsFamily || field.IsFamilyOrAssembly; + + private static bool IsStatic(PropertyInfo property) => + (property.GetMethod ?? property.SetMethod).IsStatic; + + private static string Static(bool isStatic) => isStatic ? "static " : string.Empty; + + private static string Mutability(FieldInfo field) => + field.IsLiteral ? "const " + : field.IsInitOnly ? "readonly " + : string.Empty; + + /// + /// Losing a setter, or narrowing one to protected, breaks callers as surely as losing the + /// property. + /// + private static string Accessor(string name, MethodInfo accessor) + { + if (!IsVisible(accessor)) + { + return string.Empty; + } + + return accessor.IsPublic ? $" {name};" + : accessor.IsFamilyOrAssembly ? $" protected internal {name};" + : $" protected {name};"; + } + + private static string Inheritance(MethodInfo method) + { + if (method.IsAbstract) + { + return "abstract "; + } + + if (!method.IsVirtual) + { + return string.Empty; + } + + // GetBaseDefinition is unavailable under a MetadataLoadContext, so the slot is read + // from the metadata flag instead: NewSlot means the method introduces one. + var introduces = method.Attributes.HasFlag(MethodAttributes.NewSlot); + return introduces ? "virtual " + : method.IsFinal ? "sealed override " + : "override "; + } + + private static string Arity(MethodInfo method) => + method.IsGenericMethodDefinition + ? "<" + string.Join(", ", method.GetGenericArguments().Select(Constraint)) + ">" + : string.Empty; + + private static string Constraint(Type parameter) + { + var constraints = parameter + .GetGenericParameterConstraints() + .Select(Name) + .ToList(); + + var attributes = parameter.GenericParameterAttributes; + if (attributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint)) + { + constraints.Insert(0, "class"); + } + if (attributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint)) + { + constraints.Insert(0, "struct"); + } + if (attributes.HasFlag(GenericParameterAttributes.DefaultConstructorConstraint)) + { + constraints.Add("new()"); + } + + return constraints.Count == 0 + ? parameter.Name + : $"{parameter.Name} : {string.Join(", ", constraints)}"; + } + + private static bool IsAccessor(Type type, MethodInfo method) => + type.GetProperties( + BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.Instance + | BindingFlags.Static + | BindingFlags.DeclaredOnly + ) + .Any(p => p.GetMethod == method || p.SetMethod == method) + || type.GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .Any(e => e.AddMethod == method || e.RemoveMethod == method); + + // protected internal is externally visible too: a type in another assembly can derive and + // reach it, so narrowing one is a breaking change like any other. + private static bool IsVisible(Type type) => + type.IsPublic || type.IsNestedPublic || type.IsNestedFamily || type.IsNestedFamORAssem; + + private static bool IsVisible(MethodBase method) => + method != null && (method.IsPublic || method.IsFamily || method.IsFamilyOrAssembly); + + /// + /// A model that stops being abstract, or loses a base type an app switched on, is a + /// breaking change the member lists alone would not show. + /// + private static string Abstractness(Type type) + { + if (type.IsEnum || type.IsInterface || type.IsValueType) + { + return string.Empty; + } + + return type.IsAbstract && type.IsSealed ? "static " + : type.IsAbstract ? "abstract " + : type.IsSealed ? "sealed " + : string.Empty; + } + + private static string Bases(Type type) + { + var bases = new List(); + + if (type.BaseType != null && type.BaseType.Name != "Object" && !type.IsEnum) + { + bases.Add(Name(type.BaseType)); + } + + bases.AddRange(type.GetInterfaces().Select(Name).OrderBy(x => x, StringComparer.Ordinal)); + + return bases.Count == 0 ? string.Empty : " : " + string.Join(", ", bases); + } + + private static string Kind(Type type) => + type.IsEnum ? "enum" + : type.IsInterface ? "interface" + : type.IsValueType ? "struct" + : "class"; + + private static string Parameters(MethodBase method) => + string.Join(", ", method.GetParameters().Select(Parameter)); + + /// + /// Direction and default matter: adding a default is source-compatible but removing one is + /// not, and neither is turning a value parameter into a ref. + /// + private static string Parameter(ParameterInfo parameter) + { + var direction = + parameter.IsOut ? "out " + : parameter.ParameterType.IsByRef ? (parameter.IsIn ? "in " : "ref ") + : string.Empty; + + var type = parameter.ParameterType.IsByRef + ? Name(parameter.ParameterType.GetElementType()) + : Name(parameter.ParameterType); + + var optional = parameter.HasDefaultValue + ? " = " + (parameter.RawDefaultValue?.ToString() ?? "null") + : string.Empty; + + return $"{direction}{type} {parameter.Name}{optional}"; + } + + /// + /// Type names without assembly identity: the package is read from a second, metadata-only + /// assembly, so its types would never compare equal to this project's by qualified name. + /// + private static string Name(Type type) + { + // A type parameter is named by itself: it belongs to the signature, not to a namespace. + if (type.IsGenericParameter) + { + return type.Name; + } + + // A nested generic's own name carries no arity marker, and a constructed type built + // from an outer type's parameters has none of its own. + if (type.IsGenericType && type.Name.IndexOf('`') >= 0) + { + var name = type.Name.Substring(0, type.Name.IndexOf('`')); + var arguments = string.Join(", ", type.GetGenericArguments().Select(Name)); + return $"{Namespace(type)}{name}<{arguments}>"; + } + + if (type.IsArray) + { + return Name(type.GetElementType()) + "[]"; + } + + return Namespace(type) + type.Name; + } + + private static string Namespace(Type type) => + string.IsNullOrEmpty(type.Namespace) ? string.Empty : type.Namespace + "."; + + private static string Built(string project) => + Path.Combine(ProjectDirectory(), "..", "surface", project, "bin", "Debug", "net8.0"); + + private static MetadataLoadContext Open(out Assembly package) + { + var built = Built("package"); + + var assemblies = Directory + .GetFiles(built, "*.dll") + .Concat( + Directory.GetFiles(Path.GetDirectoryName(typeof(object).Assembly.Location), "*.dll") + ) + .GroupBy(Path.GetFileName) + .Select(group => group.First()) + .ToList(); + + var context = new MetadataLoadContext(new PathAssemblyResolver(assemblies)); + package = context.LoadFromAssemblyPath(Path.Combine(built, "AdaptySDK.Surface.dll")); + return context; + } + + private static string ProjectDirectory([CallerFilePath] string callerPath = null) => + Path.GetDirectoryName(callerPath); + } +} diff --git a/tests/AdaptySDK.NextTests/RequestParityTests.cs b/tests/AdaptySDK.NextTests/RequestParityTests.cs new file mode 100644 index 0000000..97071e5 --- /dev/null +++ b/tests/AdaptySDK.NextTests/RequestParityTests.cs @@ -0,0 +1,231 @@ +using AdaptySDK.TestSupport; +using AdaptySDK.Serialization; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// Outgoing payloads: the same objects serialized through Newtonsoft have to produce what the + /// native side received before. + /// + [TestFixture] + public class RequestParityTests + { + /// + /// The contract declares a format for this one - YYYY-MM-dd - and it is built by + /// hand rather than through the date converter, which is where padding gets lost. + /// + /// + /// The sample the snapshots use is Ada Lovelace's birthday, whose month and day are both + /// two digits, so no snapshot can tell a padded writer from an unpadded one. This picks a + /// date where it shows. + /// + [Test] + public void BirthdayCarriesTheContractFormat() + { + var parameters = new AdaptyProfileParameters.Builder() + .SetBirthday(new System.DateTime(1990, 3, 7)) + .Build(); + + Assert.That( + AdaptyJson.Serialize(parameters), + Does.Contain("\"birthday\":\"1990-03-07\"") + ); + } + + [Test] + public void SubscriptionUpdateParameters() + { + var parameters = new AdaptySubscriptionUpdateParameters( + "com.adapty.sample.monthly", + AdaptySubscriptionUpdateReplacementMode.ChargeProratedPrice + ); + + Snapshots.Matches( + "request-subscription-update", + Snapshots.Canonical(AdaptyJson.Serialize(parameters)) + ); + } + + [Test] + public void PurchaseParametersFull() + { + var parameters = new AdaptyPurchaseParametersBuilder() + .SetSubscriptionUpdateParams( + new AdaptySubscriptionUpdateParameters( + "com.adapty.sample.monthly", + AdaptySubscriptionUpdateReplacementMode.Deferred + ) + ) + .SetIsOfferPersonalized(true) + .Build(); + + Snapshots.Matches( + "request-purchase-parameters-full", + Snapshots.Canonical(AdaptyJson.Serialize(parameters)) + ); + } + + [Test] + public void PurchaseParametersEmpty() + { + var parameters = new AdaptyPurchaseParametersBuilder().Build(); + + Snapshots.Matches( + "request-purchase-parameters-empty", + Snapshots.Canonical(AdaptyJson.Serialize(parameters)) + ); + } + + /// + /// Kids Mode forces apple_idfa_collection_disabled on iOS, because the trait has + /// compiled IDFA out of the binary and the request has to say so. That is the whole of its + /// effect on this layer, so only the configuration requests get a second approved form. + /// + /// + /// Without this the three snapshots below would be pinned by nothing under the define: the + /// approved files hold the flag as false, so a Kids Mode run failed all three and there was + /// no form of them anyone had approved. The flag is the only thing the define changes that + /// this layer can see, and it is the one thing a Kids Category build cannot get wrong. + /// + private static string Configured(string name) => +#if UNITY_IOS && ADAPTY_KIDS_MODE + name + "-kids"; +#else + name; +#endif + + [Test] + public void Configuration() => + Snapshots.Matches( + Configured("request-configuration"), + Snapshots.Canonical(AdaptyJson.Serialize(Samples.Configuration())) + ); + + [Test] + public void ConfigurationWithEmptyIdentity() => + Snapshots.Matches( + Configured("request-configuration-empty-identity"), + Snapshots.Canonical( + AdaptyJson.Serialize(Samples.ConfigurationWithEmptyIdentity()) + ) + ); + + [Test] + public void ConfigurationWithDefaultCluster() => + Snapshots.Matches( + Configured("request-configuration-default-cluster"), + Snapshots.Canonical( + AdaptyJson.Serialize(Samples.ConfigurationWithDefaultCluster()) + ) + ); + + [Test] + public void ProfileParameters() => + Snapshots.Matches( + "request-profile-parameters", + Snapshots.Canonical(AdaptyJson.Serialize(Samples.ProfileParameters())) + ); + + [Test] + public void DialogConfiguration() => + Snapshots.Matches( + "request-dialog-configuration", + Snapshots.Canonical(AdaptyJson.Serialize(Samples.DialogConfiguration())) + ); + + [Test] + public void DialogConfigurationMinimal() => + Snapshots.Matches( + "request-dialog-configuration-minimal", + Snapshots.Canonical(AdaptyJson.Serialize(Samples.DialogConfigurationMinimal())) + ); + + [Test] + public void FetchPolicyDefault() => + Snapshots.Matches( + "request-fetch-policy-default", + Snapshots.Canonical(AdaptyJson.Serialize(Samples.FetchPolicyDefault())) + ); + + [Test] + public void FetchPolicyWithMaxAge() => + Snapshots.Matches( + "request-fetch-policy-max-age", + Snapshots.Canonical(AdaptyJson.Serialize(Samples.FetchPolicyWithMaxAge())) + ); + + [TestCase("products-full", 0, "request-product-with-offer")] + [TestCase("products-full", 1, "request-product-plain")] + public void PaywallProduct(string fixture, int index, string snapshot) + { + var products = AdaptyJson.Deserialize>( + Snapshots.LoadResponse(fixture) + ); + + Snapshots.Matches( + snapshot, + Snapshots.Canonical( + AdaptyJson.Serialize(new AdaptyPaywallProductRequest(products[index])) + ) + ); + } + + [Test] + public void ProductIdentifier() => + Snapshots.Matches( + "request-product-identifier", + Snapshots.Canonical(AdaptyJson.Serialize(Samples.ProductIdentifier())) + ); + + [Test] + public void ProductIdentifierWithoutBasePlan() => + Snapshots.Matches( + "request-product-identifier-no-base-plan", + Snapshots.Canonical( + AdaptyJson.Serialize(Samples.ProductIdentifierWithoutBasePlan()) + ) + ); + + /// + /// The same request, from an empty base plan rather than none — the identifier normalizes it + /// at construction, so the two share an approved file. + /// + [Test] + public void ProductIdentifierWithEmptyBasePlan() => + Snapshots.Matches( + "request-product-identifier-no-base-plan", + Snapshots.Canonical( + AdaptyJson.Serialize(Samples.ProductIdentifierWithEmptyBasePlan()) + ) + ); + + [TestCase("flow-full")] + [TestCase("flow-minimal")] + public void FlowRoundTrip(string fixture) + { + var flow = AdaptyJson.Deserialize(Snapshots.LoadResponse(fixture)); + Snapshots.Matches("request-" + fixture, Snapshots.Canonical(AdaptyJson.Serialize(flow))); + } + + [TestCase("onboarding-full")] + [TestCase("onboarding-minimal")] + public void OnboardingRoundTrip(string fixture) + { + var onboarding = AdaptyJson.Deserialize( + Snapshots.LoadResponse(fixture) + ); + Snapshots.Matches( + "request-" + fixture, + Snapshots.Canonical(AdaptyJson.Serialize(onboarding)) + ); + } + + [Test] + public void CustomAssets() => + Snapshots.Matches( + "request-custom-assets", + Snapshots.Canonical(AdaptyJson.Serialize(Samples.CustomAssets())) + ); + } +} diff --git a/tests/AdaptySDK.NextTests/SerializationInfrastructureTests.cs b/tests/AdaptySDK.NextTests/SerializationInfrastructureTests.cs new file mode 100644 index 0000000..08de8cf --- /dev/null +++ b/tests/AdaptySDK.NextTests/SerializationInfrastructureTests.cs @@ -0,0 +1,455 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.Serialization; +using AdaptySDK.Serialization; +using AdaptySDK.TestSupport; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// The Newtonsoft infrastructure: the resolver, the converters and the settings behind + /// AdaptyJson. Confirmed to behave identically on an IL2CPP player — see tests/aot-probe. + /// + [TestFixture] + public class SerializationInfrastructureTests + { + [DataContract] + private class Sample + { + [DataMember(Name = "required_field", IsRequired = true)] + public readonly string RequiredField; + + [DataMember(Name = "optional_field")] + public readonly string OptionalField; + + [DataMember(Name = "activated_at")] + public readonly DateTime? ActivatedAt; + + [DataMember(Name = "offer_type")] + public readonly SampleOfferType OfferType; + + [DataMember(Name = "index")] + public readonly int Index; + } + + private enum SampleOfferType + { + [EnumMember(Value = "unknown")] + Unknown = 0, + + [EnumMember(Value = "win_back")] + WinBack, + } + + private enum StrictEnum + { + [EnumMember(Value = "first")] + First, + } + + [DataContract] + private class StrictHolder + { + [DataMember(Name = "value")] + public readonly StrictEnum Value; + } + + /// + /// Stands in for AdaptyUICreateFlowViewParameters.CustomTimers, the one place an app + /// hands the SDK a date of its own. + /// + [DataContract] + private class Timers + { + [DataMember(Name = "at")] + public DateTime At; + } + + [Test] + public void ReadonlyFieldsAreAssigned() + { + var sample = AdaptyJson.Deserialize( + "{\"required_field\":\"r\",\"optional_field\":\"o\",\"index\":7}" + ); + + Assert.Multiple(() => + { + Assert.That(sample.RequiredField, Is.EqualTo("r")); + Assert.That(sample.OptionalField, Is.EqualTo("o")); + Assert.That(sample.Index, Is.EqualTo(7)); + }); + } + + [Test] + public void MissingRequiredFieldThrows() => + Assert.Throws( + () => AdaptyJson.Deserialize("{\"optional_field\":\"o\"}") + ); + + /// + /// DataMember.IsRequired alone maps to Required.AllowNull, which would let an explicit null + /// through; AdaptyContractResolver raises it to Required.Always. + /// + [Test] + public void NullInRequiredFieldThrows() => + Assert.Throws( + () => AdaptyJson.Deserialize("{\"required_field\":null}") + ); + + /// + /// The wire is UTC and the public API is local: a date read from a response is the same + /// instant, expressed on the machine's clock, so an app compares it against + /// DateTime.Now. + /// + [Test] + public void DatesReachTheAppAsLocalTime() + { + var parsed = AdaptyJson + .Deserialize( + "{\"required_field\":\"r\",\"activated_at\":\"2026-07-30T10:00:00.000Z\"}" + ) + .ActivatedAt.Value; + + Assert.Multiple(() => + { + Assert.That(parsed.Kind, Is.EqualTo(DateTimeKind.Local)); + Assert.That( + parsed.ToUniversalTime(), + Is.EqualTo(new DateTime(2026, 7, 30, 10, 0, 0, DateTimeKind.Utc)) + ); + }); + } + + /// + /// The same invariant on the path production actually uses. A reply from native code is a + /// document parsed before anything typed is built, and a reader that parses dates while + /// building the tree would settle their kind before any converter is consulted. + /// + [Test] + public void DatesReachTheAppAsLocalTimeThroughAResponse() + { + var reply = AdaptyResponse.Parse( + "{\"success\":" + Snapshots.LoadResponse("profile-full") + "}" + ); + + Assert.That(reply.Error, Is.Null); + + var premium = reply.Value.AccessLevels["premium"]; + + Assert.Multiple(() => + { + Assert.That( + premium.ActivatedAt.Kind, + Is.EqualTo(DateTimeKind.Local), + "a date came out of the reply envelope in the wrong zone" + ); + Assert.That( + premium.ActivatedAt.ToUniversalTime(), + Is.EqualTo(new DateTime(2026, 1, 15, 9, 30, 0, DateTimeKind.Utc)), + "the zone was right and the instant was not" + ); + }); + } + + /// + /// A payload with anything after it is not a payload. The reply from native code is checked + /// here and nowhere else, so a second document must not be able to hide behind a comment + /// and leave the first one standing in for the whole thing. + /// + [TestCase("{} {\"a\":1}", TestName = "a second document")] + [TestCase("{}/* c */{\"a\":1}", TestName = "a second document behind a comment")] + [TestCase("{} nonsense", TestName = "trailing text")] + public void ADocumentWithAnythingAfterItIsRejected(string json) => + Assert.That( + () => AdaptyJson.ParseDocument(json), + Throws.InstanceOf() + ); + + [TestCase("{\"a\":1}", TestName = "well formed")] + [TestCase("{}/* c */", TestName = "a trailing comment and nothing else")] + public void ADocumentWithNothingAfterItIsAccepted(string json) => + Assert.That(() => AdaptyJson.ParseDocument(json), Throws.Nothing); + + /// + /// The converter's belt, tested on its own because nothing else can reach it. + /// + /// + /// AdaptyJson.ParseDocument keeps a reader from settling a date's kind before the + /// converter sees it, so on the SDK's own paths this branch never runs - which also means + /// the two tests above stay green whether or not the belt is there, and this one is what + /// holds it. JToken.Parse is used deliberately: its reader is the one that parses + /// dates while building the tree. + /// + [Test] + public void APreParsedDateIsBroughtBackToTheContract() + { + var preParsed = Newtonsoft.Json.Linq.JToken.Parse( + "{\"required_field\":\"r\",\"activated_at\":\"2026-07-30T10:00:00.000Z\"}" + ); + + Assert.That( + preParsed["activated_at"].Type, + Is.EqualTo(Newtonsoft.Json.Linq.JTokenType.Date), + "sanity: this reader is supposed to have parsed the date already" + ); + + var parsed = preParsed.ToObject(AdaptyJson.CreateSerializer()).ActivatedAt.Value; + + Assert.Multiple(() => + { + Assert.That(parsed.Kind, Is.EqualTo(DateTimeKind.Local)); + Assert.That( + parsed.ToUniversalTime(), + Is.EqualTo(new DateTime(2026, 7, 30, 10, 0, 0, DateTimeKind.Utc)) + ); + }); + } + + /// + /// A date string with no Z and no offset is read as UTC, like every other date. + /// + /// + /// The contract's format always carries a designator and neither native SDK omits one, so + /// this shape does not arrive today. It is pinned because the converter has two branches + /// that have to agree: covers the + /// pre-parsed one, which treats an unspecified kind as UTC, and the string branch used to + /// disagree - DateTime.Parse's default styles hand back Unspecified + /// unconverted, which moves the instant by the device's offset and contradicts the + /// convention the type documents. + /// + [Test] + public void ADateCarryingNoZoneIsReadAsUtc() + { + var parsed = AdaptyJson + .Deserialize( + "{\"required_field\":\"r\",\"activated_at\":\"2026-07-30T10:00:00.000\"}" + ) + .ActivatedAt.Value; + + Assert.Multiple(() => + { + Assert.That(parsed.Kind, Is.EqualTo(DateTimeKind.Local)); + Assert.That( + parsed.ToUniversalTime(), + Is.EqualTo(new DateTime(2026, 7, 30, 10, 0, 0, DateTimeKind.Utc)) + ); + }); + } + + /// + /// The other half of the same convention: whatever the app hands over goes out as UTC. + /// + /// + /// The unspecified case is the one that matters and the one no other test reaches - it is + /// how an app writes a custom timer, new DateTime(2026, 7, 30, 22, 0, 0), and it has + /// to mean 22:00 on the user's clock rather than 22:00 UTC. A value the SDK itself read is + /// already local, so it cannot show the difference. + /// + [Test] + public void DatesTheAppSuppliesAreWrittenAsUtc() + { + var wall = new DateTime(2026, 7, 30, 22, 0, 0); + var offset = TimeZoneInfo.Local.GetUtcOffset(wall); + + if (offset == TimeSpan.Zero) + { + Assert.Ignore( + "the host is on UTC, where converting and relabelling agree - " + + "run with TZ set to a zone with an offset, as CI does" + ); + } + + // Not through ToUniversalTime: that is the very call under test, and comparing a value + // against itself would pass either way. + var expected = (wall - offset).ToString( + "yyyy-MM-ddTHH:mm:ss.fffZ", + CultureInfo.InvariantCulture + ); + + Assert.Multiple(() => + { + Assert.That(wall.Kind, Is.EqualTo(DateTimeKind.Unspecified), "sanity"); + Assert.That( + AdaptyJson.Serialize(new Timers { At = wall }), + Does.Contain("\"at\":\"" + expected + "\""), + "an unspecified date was not read as the user's local clock" + ); + }); + } + + [Test] + public void DatesAreWrittenAsUtcWithMilliseconds() + { + var sample = AdaptyJson.Deserialize( + "{\"required_field\":\"r\",\"activated_at\":\"2026-07-30T10:00:00.000Z\"}" + ); + + Assert.That( + AdaptyJson.Serialize(sample), + Does.Contain("\"activated_at\":\"2026-07-30T10:00:00.000Z\"") + ); + } + + [Test] + public void EnumsUseContractNames() + { + var sample = AdaptyJson.Deserialize( + "{\"required_field\":\"r\",\"offer_type\":\"win_back\"}" + ); + + Assert.That(sample.OfferType, Is.EqualTo(SampleOfferType.WinBack)); + Assert.That(AdaptyJson.Serialize(sample), Does.Contain("\"offer_type\":\"win_back\"")); + } + + /// + /// A declared "unknown" catches nothing: where the contract lists the string it is a value + /// like any other, and a value outside the contract throws whether or not the enum has one. + /// + [Test] + public void UnknownEnumValueThrowsWhateverTheEnumDeclares() + { + var declared = AdaptyJson.Deserialize( + "{\"required_field\":\"r\",\"offer_type\":\"unknown\"}" + ); + Assert.That(declared.OfferType, Is.EqualTo(SampleOfferType.Unknown)); + + Assert.Throws( + () => + AdaptyJson.Deserialize( + "{\"required_field\":\"r\",\"offer_type\":\"brand_new_from_native\"}" + ) + ); + + Assert.Throws( + () => AdaptyJson.Deserialize("{\"value\":\"brand_new\"}") + ); + } + + /// + /// A number is not a value of a string enum, whatever it would map to. + /// + [Test] + public void NumberInAStringEnumThrows() => + Assert.Throws( + () => AdaptyJson.Deserialize("{\"value\":0}") + ); + + [Test] + public void MissingEnumFieldBecomesTheZeroMember() + { + var sample = AdaptyJson.Deserialize("{\"required_field\":\"r\"}"); + + Assert.That(sample.OfferType, Is.EqualTo(SampleOfferType.Unknown)); + } + + /// + /// Loose JSON has to keep yielding double and nested dictionaries, as SimpleJSON did: + /// AdaptyRemoteConfig.Dictionary is public API. + /// + [Test] + public void LooseObjectsMatchTheCurrentShape() + { + const string json = "{\"n\":42,\"s\":\"x\",\"flag\":true,\"nested\":{\"k\":1},\"list\":[1,2]}"; + + var parsed = AdaptyJson.DeserializeRemoteConfigDictionary(json); + + Assert.Multiple(() => + { + Assert.That(parsed["n"], Is.EqualTo(42d).And.TypeOf()); + Assert.That(parsed["s"], Is.EqualTo("x")); + Assert.That(parsed["flag"], Is.EqualTo(true)); + Assert.That(parsed["nested"], Is.TypeOf>()); + Assert.That(parsed["list"], Is.TypeOf>()); + }); + } + + /// + /// The other side of that border. The loose converter is not in the shared settings, so a + /// dictionary read through the ordinary path gets Newtonsoft's own shapes. + /// + [Test] + public void LooseObjectsOutsideTheirMembersKeepNewtonsoftShapes() + { + var parsed = AdaptyJson.Deserialize>( + "{\"n\":42,\"nested\":{\"k\":1},\"list\":[1,2]}" + ); + + Assert.Multiple(() => + { + Assert.That(parsed["n"], Is.TypeOf()); + Assert.That(parsed["nested"], Is.TypeOf()); + Assert.That(parsed["list"], Is.TypeOf()); + }); + } + + /// + /// The second member the converter serves, and the one no fixture can cover: the only + /// number among the profile fixture's custom attributes is 12.5, which is a double whether + /// or not the converter runs, and the snapshot prints an integral double and a long alike. + /// + [Test] + public void ProfileCustomAttributesKeepTheirDoubles() + { + var payload = Newtonsoft.Json.Linq.JObject.Parse( + Snapshots.LoadResponse("profile-minimal") + ); + payload["custom_attributes"] = Newtonsoft.Json.Linq.JObject.Parse( + "{\"score\":12,\"name\":\"x\"}" + ); + + var profile = AdaptyJson.Deserialize(payload.ToString()); + + Assert.Multiple(() => + { + Assert.That(profile.CustomAttributes["score"], Is.EqualTo(12d).And.TypeOf()); + Assert.That(profile.CustomAttributes["name"], Is.EqualTo("x")); + }); + } + + /// + /// Date-looking strings inside loose payloads must survive as strings. + /// + [Test] + public void DateLikeStringsInLoosePayloadsStayStrings() + { + var parsed = AdaptyJson.DeserializeRemoteConfigDictionary( + "{\"released_at\":\"2026-07-30T10:00:00.000Z\"}" + ); + + Assert.That(parsed["released_at"], Is.TypeOf()); + } + + [Test] + public void NullValuesAreOmittedOnWrite() + { + var sample = AdaptyJson.Deserialize("{\"required_field\":\"r\"}"); + + Assert.That(AdaptyJson.Serialize(sample), Does.Not.Contain("optional_field")); + } + + /// + /// Zero and false must still be written: EmitDefaultValue = false would drop them, which is + /// why the models do not use it. + /// + [Test] + public void ZeroIsStillWritten() + { + var sample = AdaptyJson.Deserialize("{\"required_field\":\"r\",\"index\":0}"); + + Assert.That(AdaptyJson.Serialize(sample), Does.Contain("\"index\":0")); + } + + [Test] + public void UnknownFieldsFromNewerNativeSdkAreIgnored() + { + var sample = AdaptyJson.Deserialize( + "{\"required_field\":\"r\",\"field_from_the_future\":123}" + ); + + Assert.That(sample.RequiredField, Is.EqualTo("r")); + } + } +} diff --git a/tests/AdaptySDK.NextTests/SourceConventionTests.cs b/tests/AdaptySDK.NextTests/SourceConventionTests.cs new file mode 100644 index 0000000..009766f --- /dev/null +++ b/tests/AdaptySDK.NextTests/SourceConventionTests.cs @@ -0,0 +1,208 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// Rules about how the runtime sources are arranged. Metadata cannot answer either of these: + /// partial does not survive compilation, and a file's directory is not a property of the + /// types in it — so both are asked of the sources themselves. + /// + [TestFixture] + public class SourceConventionTests + { + private static readonly Regex Declaration = new Regex( + @"^(?[ \t]*)(?:(?:public|internal|private|protected|static|sealed|abstract)[ \t]+)*partial[ \t]+(?:class|struct|interface)[ \t]+(?\w+)" + ); + + /// + /// partial on a type that lives in one file promises a second part that does not + /// exist, and sends the reader looking for it. The six that keep it are split for a reason: + /// a nested builder, a nested model, or - for Adapty and AdaptyUI - the + /// deprecated half held apart under Obsolete/. + /// + [Test] + public void EveryPartialTypeIsActuallySplit() + { + var parts = new Dictionary>(); + + foreach (var file in Sources()) + { + var nesting = new List>(); + + foreach (var line in File.ReadAllLines(file)) + { + var match = Declaration.Match(line); + if (!match.Success) + { + continue; + } + + var indent = match.Groups["indent"].Value.Length; + while (nesting.Count > 0 && nesting[nesting.Count - 1].Key >= indent) + { + nesting.RemoveAt(nesting.Count - 1); + } + + var name = match.Groups["name"].Value; + var identity = string.Join( + ".", + nesting.Select(level => level.Value).Concat(new[] { name }) + ); + nesting.Add(new KeyValuePair(indent, name)); + + if (!parts.TryGetValue(identity, out var files)) + { + parts[identity] = files = new List(); + } + files.Add(Path.GetFileName(file)); + } + } + + var alone = parts + .Where(entry => entry.Value.Count == 1) + .Select(entry => $"{entry.Key} ({entry.Value[0]})") + .OrderBy(name => name, System.StringComparer.Ordinal) + .ToList(); + + Assert.That( + alone, + Is.Empty, + "these are partial but declared once, so the modifier points at nothing:\n " + + string.Join("\n ", alone) + ); + } + + /// + /// Removing the deprecated API should be a directory deletion plus whatever then fails to + /// compile. That only holds while the attribute and the folder travel together. + /// + [Test] + public void EveryObsoleteMemberLivesUnderObsolete() + { + var outside = Sources() + .Where(file => !file.Replace('\\', '/').Contains("/Obsolete/")) + .Where(file => Regex.IsMatch(File.ReadAllText(file), @"\[(System\.)?Obsolete")) + .Select(Path.GetFileName) + .OrderBy(name => name, System.StringComparer.Ordinal) + .ToList(); + + Assert.That( + outside, + Is.Empty, + "these carry [Obsolete] outside Runtime/Obsolete:\n " + string.Join("\n ", outside) + ); + } + + /// + /// The conventions swept in one pass, kept in one test because each is a single line and + /// they fail the same way: a file that reintroduces the habit. + /// + /// + /// Deliberately not a rule about == null. That reads as the same tidiness, and it is + /// not: UnityEngine.Object overloads the operator to answer true for a destroyed + /// native object, and is null bypasses the overload - so a blanket ban would push + /// correct Unity code into being wrong. + /// + [Test] + public void TheSourcesKeepTheirShape() + { + var headers = new List(); + var boms = new List(); + var scopedUsings = new List(); + var negatedPatterns = new List(); + + foreach (var file in Sources()) + { + var name = Path.GetFileName(file); + var text = File.ReadAllText(file); + + // Read as bytes: File.ReadAllText strips the mark while decoding, and + // StartsWith("\uFEFF") is culture-sensitive, where a zero-weight character matches + // the start of every string - both would report the opposite of the truth. + var head = new byte[3]; + using (var stream = File.OpenRead(file)) + { + if (stream.Read(head, 0, 3) == 3 + && head[0] == 0xEF && head[1] == 0xBB && head[2] == 0xBF) + { + boms.Add(name); + } + } + + if (Regex.IsMatch(text, @"^//\s*\r?\n//\s+[\w.]+\.cs\r?\n")) + { + headers.Add(name); + } + + foreach (var line in Code(file)) + { + if (Regex.IsMatch(line, @"^\s+using [\w.= ]+;\s*$")) + { + scopedUsings.Add($"{name}: {line.Trim()}"); + } + + if (Regex.IsMatch(line, @"!\([^()]*\bis\b")) + { + negatedPatterns.Add($"{name}: {line.Trim()}"); + } + } + } + + Assert.Multiple(() => + { + Assert.That(headers, Is.Empty, "these carry the old file header block"); + Assert.That(boms, Is.Empty, "these start with a byte order mark"); + Assert.That(scopedUsings, Is.Empty, "these declare a using inside the namespace"); + Assert.That( + negatedPatterns, + Is.Empty, + "these negate a type pattern with ! instead of writing `is not`" + ); + }); + } + + /// + /// A sweep that stops finding what it sweeps passes silently. + /// + [Test] + public void TheSweepStillSeesTheRuntime() + { + var files = Sources().ToList(); + + Assert.Multiple(() => + { + Assert.That(files.Count, Is.GreaterThan(50), "far fewer sources than the package has"); + Assert.That( + files.Count(file => file.Replace('\\', '/').Contains("/Obsolete/")), + Is.GreaterThan(5), + "the deprecated tree is no longer being found" + ); + Assert.That( + files.Count(file => File.ReadAllLines(file).Any(Declaration.IsMatch)), + Is.GreaterThan(5), + "the partial rule no longer matches any declaration" + ); + }); + } + + private static IEnumerable Sources() => + Directory.EnumerateFiles(Package(), "*.cs", SearchOption.AllDirectories); + + /// + /// Lines that are not a comment, so a rule about code does not trip over prose. + /// + private static IEnumerable Code(string file) => + File.ReadAllLines(file).Where(line => !line.TrimStart().StartsWith("//")); + + private static string Package() => + Path.Combine(ProjectDirectory(), "..", "..", "Packages", "com.adapty.unity-sdk"); + + private static string ProjectDirectory([CallerFilePath] string callerPath = null) => + Path.GetDirectoryName(callerPath); + } +} diff --git a/tests/AdaptySDK.NextTests/StrippingGuardTests.cs b/tests/AdaptySDK.NextTests/StrippingGuardTests.cs new file mode 100644 index 0000000..ddf5e07 --- /dev/null +++ b/tests/AdaptySDK.NextTests/StrippingGuardTests.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// Every type the serializer reaches by reflection has to carry [Preserve]. Managed + /// stripping otherwise removes it, and the failure only shows on a device, the first time a + /// response carries the type. Asked of the metadata here, so it runs without a Unity build. + /// + [TestFixture] + public class StrippingGuardTests + { + private const string Preserve = "UnityEngine.Scripting.PreserveAttribute"; + private const string DataContract = "System.Runtime.Serialization.DataContractAttribute"; + private const string DataMember = "System.Runtime.Serialization.DataMemberAttribute"; + private const string OnDeserialized = + "System.Runtime.Serialization.OnDeserializedAttribute"; + + /// + /// Bases whose subclasses a converter constructs by name. They carry no contract attribute + /// of their own, so they would not be caught by the rules above. + /// + private static readonly string[] PolymorphicRoots = + { + "AdaptyCustomAsset", + "AdaptyOnboardingsAnalyticsEvent", + "AdaptyOnboardingsStateUpdatedParams", + "AdaptyOnboardingsInput", + }; + + [Test] + public void EveryReflectedTypeIsPreserved() + { + using var context = Open(out var package); + + var missing = Reflected(package) + .Where(type => !IsPreserved(type)) + .Select(type => type.FullName) + .OrderBy(name => name, StringComparer.Ordinal) + .ToList(); + + Assert.That( + missing, + Is.Empty, + "these are created or read by reflection and would be stripped:\n " + + string.Join("\n ", missing) + ); + } + + /// + /// A type's [Preserve] does not extend to its methods, so every member the serializer + /// reaches through one needs its own. Fields are not listed: they survive on the type + /// attribute alone, as measured on a stripped player. + /// + [Test] + public void EveryReflectedMemberIsPreserved() + { + using var context = Open(out var package); + + var missing = Reflected(package) + .SelectMany(ReflectedMembers) + .Where(member => !Has(member, Preserve)) + .Select(member => $"{member.DeclaringType.Name}.{member.Name}") + .OrderBy(name => name, StringComparer.Ordinal) + .ToList(); + + Assert.That( + missing, + Is.Empty, + "these are read by the serializer through a method and would be stripped:\n " + + string.Join("\n ", missing) + ); + } + + /// + /// The members a serializer invokes rather than reads directly: a contract property is read + /// through its getter, and a deserialization callback is what enforces a requirement no + /// attribute can state. + /// + private static IEnumerable ReflectedMembers(Type type) + { + const BindingFlags Declared = + BindingFlags.Instance + | BindingFlags.Static + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly; + + foreach (var property in type.GetProperties(Declared)) + { + if (Has(property, DataMember)) + { + yield return property; + } + } + + foreach (var method in type.GetMethods(Declared)) + { + if (Has(method, OnDeserialized)) + { + yield return method; + } + } + } + + /// + /// A guard that stops recognising what it guards passes silently. If the rules below stop + /// matching the package, this fails before the test above starts reporting success for the + /// wrong reason. + /// + [Test] + public void TheGuardStillRecognisesThePackage() + { + using var context = Open(out var package); + + var reflected = Reflected(package).ToList(); + + Assert.Multiple(() => + { + Assert.That(reflected.Count, Is.GreaterThan(70), "far fewer types than the package has"); + + Assert.That( + reflected.SelectMany(ReflectedMembers).Count(), + Is.GreaterThan(30), + "the member rules no longer match the contract properties they are meant to guard" + ); + + Assert.That( + reflected.SelectMany(ReflectedMembers).Any(member => Has(member, OnDeserialized)), + Is.True, + "the deserialization-callback rule no longer matches any method" + ); + + foreach (var name in new[] + { + "AdaptyProfile", + "AdaptyFlow", + "AdaptyPaywallProduct", + "AdaptySubscriptionPeriod", + "AdaptyPaymentMode", + "AdaptyOnboarding", + "AdaptyCustomAssetLocalImageFile", + "AdaptyInstallationStatus", + "AdaptyOnboardingsSelectParams", + // A numeric-contract enum: it carries no [EnumMember], so an enum rule written + // in terms of that attribute would stop seeing it. + "AdaptyErrorCode", + }) + { + Assert.That( + reflected.Any(type => type.Name == name), + Is.True, + $"{name} is no longer recognised as a reflection target" + ); + } + }); + } + + /// + /// A type the serializer creates from JSON, or whose members it reads. + /// + private static IEnumerable Reflected(Assembly package) => + package + .GetTypes() + // The surface assembly compiles the Unity stubs in so the package can build without + // an Editor. They stand in for types Unity ships and the serializer never sees, so + // a rule about what stripping would remove does not apply to them. + .Where(type => type.Namespace?.StartsWith("UnityEngine") != true) + .Where(type => + Has(type, DataContract) + || type.IsEnum + || DerivesFromPolymorphicRoot(type) + ); + + private static bool DerivesFromPolymorphicRoot(Type type) + { + for (var current = type; current != null; current = current.BaseType) + { + if (PolymorphicRoots.Contains(current.Name)) + { + return true; + } + } + return false; + } + + /// + /// A nested type is covered by its declaring type's attribute — measured on a stripped + /// player, where AdaptyOnboarding's private OnboardingBuilder survived on the outer + /// attribute alone. + /// + private static bool IsPreserved(Type type) + { + for (var current = type; current != null; current = current.DeclaringType) + { + if (Has(current, Preserve)) + { + return true; + } + } + return false; + } + + // Attributes are matched by name: the assembly is read for metadata only, so the attribute + // types themselves are never loaded and cannot be compared as Type. + private static bool Has(MemberInfo member, string attribute) => + member.GetCustomAttributesData().Any(data => data.AttributeType.FullName == attribute); + + private static MetadataLoadContext Open(out Assembly package) + { + var directory = Path.Combine( + Path.GetDirectoryName(SourcePath()), + "..", + "surface", + "package", + "bin", + "Debug", + "net8.0" + ); + + // The surface project is a library, so its dependencies are not copied next to it; + // the test's own output directory has them. + var assemblies = Directory + .GetFiles(directory, "*.dll") + .Concat(Directory.GetFiles(AppContext.BaseDirectory, "*.dll")) + .Concat( + Directory.GetFiles(Path.GetDirectoryName(typeof(object).Assembly.Location), "*.dll") + ) + .GroupBy(Path.GetFileName) + .Select(group => group.First()) + .ToList(); + + var context = new MetadataLoadContext(new PathAssemblyResolver(assemblies)); + package = context.LoadFromAssemblyPath( + Path.Combine(directory, "AdaptySDK.Surface.dll") + ); + return context; + } + + private static string SourcePath([CallerFilePath] string path = null) => path; + } +} diff --git a/tests/AdaptySDK.NextTests/TransportTests.cs b/tests/AdaptySDK.NextTests/TransportTests.cs new file mode 100644 index 0000000..5e99054 --- /dev/null +++ b/tests/AdaptySDK.NextTests/TransportTests.cs @@ -0,0 +1,381 @@ +// The bridge is chosen by the same #if the SDK uses: off the editor it is a real P/Invoke or +// AndroidJavaClass with nothing behind it on a desktop test host. These fixtures drive the +// transport end to end, so they need the no-op bridge; device coverage is a separate stage. +// The platform-dependent payloads themselves - the custom asset paths - are pinned per platform +// by the request snapshots. +#if !UNITY_IOS && !UNITY_ANDROID + +using System; +using System.Collections.Generic; +using AdaptySDK.TestSupport; +using AdaptySDK.Noop; +using AdaptySDK.Serialization; +using NUnit.Framework; + +namespace AdaptySDK.NextTests +{ + /// + /// What actually crosses the bridge. The public methods assemble a request key by key, so the + /// only way to see the result is from the far side of the transport. + /// + [TestFixture] + public class TransportTests + { + private string _method; + private string _request; + private string _reply; + + [SetUp] + public void Setup() + { + _method = null; + _request = null; + _reply = "{\"success\":true}"; + + AdaptyNoop.Handler = (method, request) => + { + _method = method; + _request = request; + return _reply; + }; + } + + [TearDown] + public void TearDown() => AdaptyNoop.Handler = null; + + [Test] + public void ActivateSendsTheConfiguration() + { + Adapty.Activate(Samples.Configuration(), _ => { }); + + Assert.That(_method, Is.EqualTo("activate")); + Snapshots.Matches("transport-activate", Snapshots.Canonical(_request)); + } + + [Test] + public void GetFlowSendsThePlacementAndPolicy() + { + _reply = "{\"success\":" + Snapshots.LoadResponse("flow-minimal") + "}"; + + AdaptyFlow received = null; + Adapty.GetFlow( + "onboarding", + AdaptyPlacementFetchPolicy.ReturnCacheDataIfNotExpiredElseLoad( + TimeSpan.FromSeconds(90) + ), + TimeSpan.FromSeconds(5), + (flow, _) => received = flow + ); + + Assert.That(_method, Is.EqualTo("get_flow")); + Assert.That(received, Is.Not.Null, "the reply was not mapped back to a model"); + Assert.That(received.InstanceIdentity, Is.Not.Null); + Snapshots.Matches("transport-get-flow", Snapshots.Canonical(_request)); + } + + /// + /// The optional view parameters are merged into the request rather than nested, so this + /// pins the flattening the annotated model now performs. + /// + [Test] + public void CreateFlowViewFlattensItsOptionalParameters() + { + var flow = AdaptyJson.Deserialize(Snapshots.LoadResponse("flow-minimal")); + + AdaptyUI.CreateFlowView( + flow, + new AdaptyUICreateFlowViewParameters() + .SetLocale("es") + .SetLoadTimeout(TimeSpan.FromSeconds(12)) + .SetPreloadProducts(true) + .SetCustomTags(new Dictionary { { "NAME", "Ada" } }) + .SetCustomTimers( + new Dictionary + { + { "OFFER_END", new DateTime(2026, 7, 30, 10, 0, 0, DateTimeKind.Utc) }, + } + ) + .SetCustomAssets(Samples.CustomAssets()) + .SetEnableSafeAreaPaddings(false), + (_, __) => { } + ); + + Assert.That(_method, Is.EqualTo("adapty_ui_create_flow_view")); + Snapshots.Matches("transport-create-flow-view", Snapshots.Canonical(_request)); + } + + [Test] + public void UpdateAttributionSendsEveryValueKind() + { + Adapty.UpdateAttribution( + new Dictionary + { + { "status", "organic" }, + { "clicks", 3 }, + { "cost", 1.5 }, + { "is_retargeting", false }, + { "install_time", new DateTime(2026, 7, 30, 10, 0, 0, DateTimeKind.Utc) }, + { "campaign", null }, + { "tags", new List { "a", "b" } }, + { "nested", new Dictionary { { "k", "v" } } }, + }, + "appsflyer", + _ => { } + ); + + Assert.That(_method, Is.EqualTo("update_attribution_data")); + Snapshots.Matches("transport-update-attribution", Snapshots.Canonical(_request)); + } + + /// + /// The dictionary overload is the one public method that encodes an argument before it can + /// build a request, so it is the one place a serialization failure could escape the + /// transport's guard and be thrown at the caller instead of reported to the handler. + /// + [Test] + public void UpdateAttributionReportsAGraphItCannotEncode() + { + var loop = new Dictionary(); + loop["self"] = loop; + + AdaptyError reported = null; + + Assert.DoesNotThrow( + () => Adapty.UpdateAttribution(loop, "custom", error => reported = error) + ); + + Assert.Multiple(() => + { + Assert.That(reported, Is.Not.Null, "the completion handler was never called"); + Assert.That(reported?.Code, Is.EqualTo(AdaptyErrorCode.EncodingFailed)); + Assert.That(_method, Is.Null, "nothing should have reached the bridge"); + }); + } + + + /// + /// One case per migrated public method, so a call site cannot change what it sends - or + /// which shape it sends a model in - without a snapshot moving. + /// + /// + /// Compiling under three symbol sets proves nothing about method names, request keys or + /// which DTO a call site picked. Only the payload does. + /// + [TestCaseSource(nameof(Requests))] + public void RequestPayload(string name, string method, Action send) + { + send(); + + Assert.That(_method, Is.EqualTo(method)); + Snapshots.Matches("transport-" + name, Snapshots.Canonical(_request)); + } + + private static IEnumerable Requests() + { + var flow = AdaptyJson.Deserialize(Snapshots.LoadResponse("flow-minimal")); + var products = AdaptyJson.Deserialize>( + Snapshots.LoadResponse("products-full") + ); + var withOffer = products[0]; + var withoutOffer = products[1]; + var paywall = AdaptyJson + .Deserialize(Snapshots.LoadResponse("flow-full")) + .Paywalls[0]; + var flowView = AdaptyJson.Deserialize( + "{\"id\":\"view-1\",\"placement_id\":\"onboarding\",\"variation_id\":\"variation-0001\"}" + ); + + TestCaseData Case(string name, string method, Action send) => + new TestCaseData(name, method, send).SetName($"{{m}}({name})"); + + // The three product paths: the response model is 17 fields, the request a strict subset + // with a synthesized offer identifier, so these guard the DTO being used at all. + yield return Case( + "make-purchase-with-offer", + "make_purchase", + () => Adapty.MakePurchase(withOffer, (_, __) => { }) + ); + yield return Case( + "make-purchase-without-offer", + "make_purchase", + () => Adapty.MakePurchase(withoutOffer, (_, __) => { }) + ); + yield return Case( + "create-web-paywall-url-product", + "create_web_paywall_url", + () => Adapty.CreateWebPaywallUrl(withOffer, (_, __) => { }) + ); + yield return Case( + "open-web-paywall-product", + "open_web_paywall", + () => Adapty.OpenWebPaywall(withOffer, AdaptyWebPresentation.InAppBrowser, _ => { }) + ); + + yield return Case( + "create-web-paywall-url-paywall", + "create_web_paywall_url", + () => Adapty.CreateWebPaywallUrl(paywall, (_, __) => { }) + ); + yield return Case( + "get-paywall-products", + "get_paywall_products", + () => Adapty.GetPaywallProducts(flow, (_, __) => { }) + ); + yield return Case( + "log-show-flow", + "log_show_flow", + () => Adapty.LogShowFlow(flow, _ => { }) + ); + yield return Case( + "identify", + "identify", + () => Adapty.Identify("user-1", Guid.Empty, "obfuscated-1", _ => { }) + ); + yield return Case("logout", "logout", () => Adapty.Logout(_ => { })); + yield return Case( + "get-profile", + "get_profile", + () => Adapty.GetProfile((_, __) => { }) + ); + yield return Case( + "update-profile", + "update_profile", + () => Adapty.UpdateProfile(Samples.ProfileParameters(), _ => { }) + ); + yield return Case( + "set-log-level", + "set_log_level", + () => Adapty.SetLogLevel(AdaptyLogLevel.Verbose, _ => { }) + ); + yield return Case( + "set-fallback", + "set_fallback", + () => Adapty.SetFallback("fallback.json", _ => { }) + ); + yield return Case( + "set-integration-identifier", + "set_integration_identifiers", + () => Adapty.SetIntegrationIdentifier("appsflyer_id", "af-1", _ => { }) + ); + yield return Case( + "report-transaction", + "report_transaction", + () => Adapty.ReportTransaction("txn-1", "variation-0001", _ => { }) + ); + yield return Case( + "restore-purchases", + "restore_purchases", + () => Adapty.RestorePurchases((_, __) => { }) + ); + // iOS-only, and the reason they are here is the Editor rather than the payload: they + // used to take an #else that reported a null error, which a caller cannot tell from + // success, instead of reaching the bridge that says the SDK is not available here. + yield return Case( + "update-collecting-refund-data-consent", + "update_collecting_refund_data_consent", + () => Adapty.UpdateAppStoreCollectingRefundDataConsent(true, _ => { }) + ); + yield return Case( + "update-refund-preference", + "update_refund_preference", + () => Adapty.UpdateAppStoreRefundPreference(AdaptyRefundPreference.Grant, _ => { }) + ); + yield return Case( + "present-code-redemption-sheet", + "present_code_redemption_sheet", + () => Adapty.PresentCodeRedemptionSheet(_ => { }) + ); + + yield return Case( + "open-url", + "adapty_ui_open_url", + () => AdaptyUI.OpenUrl("https://adapty.io", AdaptyWebPresentation.ExternalBrowser, _ => { }) + ); + yield return Case( + "present-flow-view", + "adapty_ui_present_flow_view", + () => AdaptyUI.PresentFlowView(flowView, _ => { }) + ); + yield return Case( + "dismiss-flow-view", + "adapty_ui_dismiss_flow_view", + () => AdaptyUI.DismissFlowView(flowView, _ => { }) + ); + yield return Case( + "show-dialog", + "adapty_ui_show_dialog", + () => AdaptyUI.ShowDialog(flowView, Samples.DialogConfiguration(), (_, __) => { }) + ); + } + + /// + /// A reply with neither member is malformed, not an empty success: reporting it as a + /// default would mean "not premium" or "the purchase did not happen". + /// + [TestCase("{}")] + [TestCase("{\"unrelated\":1}")] + [TestCase("{\"success\":null}")] + [TestCase("[]")] + public void RepliesWithoutSuccessAreDecodingErrors(string reply) + { + _reply = reply; + + AdaptyError fromValueType = null; + var activated = true; + Adapty.IsActivated((value, error) => (activated, fromValueType) = (value, error)); + + AdaptyError fromReferenceType = null; + AdaptyProfile profile = null; + Adapty.GetProfile((value, error) => (profile, fromReferenceType) = (value, error)); + + Assert.Multiple(() => + { + Assert.That(fromValueType?.Code, Is.EqualTo(AdaptyErrorCode.DecodingFailed)); + Assert.That(activated, Is.False); + Assert.That(fromReferenceType?.Code, Is.EqualTo(AdaptyErrorCode.DecodingFailed)); + Assert.That(profile, Is.Null); + }); + } + + /// + /// A native error comes back as an AdaptyError, not as an exception. + /// + [Test] + public void ErrorRepliesAreMappedNotThrown() + { + _reply = + "{\"error\":{\"adapty_code\":2003,\"message\":\"not found\",\"detail\":\"d\"}}"; + + AdaptyError received = null; + Adapty.GetProfile((_, error) => received = error); + + Assert.That(received, Is.Not.Null); + Assert.That(received.Code, Is.EqualTo(AdaptyErrorCode.BadRequest)); + Assert.That(received.Message, Is.EqualTo("not found")); + } + + /// + /// A reply the SDK cannot read is an error too - it must never escape the callback, which + /// on iOS is a reverse-P/Invoke boundary. + /// + [TestCase("not json at all")] + [TestCase("{\"success\":{\"flow_id\":\"only-this\"}}")] + public void MalformedRepliesBecomeDecodingErrors(string reply) + { + _reply = reply; + + AdaptyError received = null; + AdaptyFlow value = null; + Assert.That( + () => Adapty.GetFlow("onboarding", (flow, error) => (value, received) = (flow, error)), + Throws.Nothing + ); + + Assert.That(value, Is.Null); + Assert.That(received, Is.Not.Null); + Assert.That(received.Code, Is.EqualTo(AdaptyErrorCode.DecodingFailed)); + } + } +} + +#endif diff --git a/tests/aot-probe/AotSerializationProbe.cs b/tests/aot-probe/AotSerializationProbe.cs new file mode 100644 index 0000000..d920111 --- /dev/null +++ b/tests/aot-probe/AotSerializationProbe.cs @@ -0,0 +1,134 @@ +using System; +using System.Reflection; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using UnityEngine; + +/// +/// Temporary probe for the Newtonsoft migration: checks on a real IL2CPP build what the plan +/// assumes from CoreCLR behaviour. Delete once the migration is done. +/// +/// The decisive question is whether Newtonsoft can assign initonly fields through reflection +/// under AOT — the whole "keep 155 readonly fields as they are" decision rests on it. +/// +public static class AotSerializationProbe +{ + [DataContract] + private class Probe + { + [DataMember(Name = "flow_id", IsRequired = true)] + public readonly string InstanceIdentity; + + [DataMember(Name = "payload_data")] + private readonly string _PayloadData; + + [DataMember(Name = "count")] + public readonly int Count; + + [DataMember(Name = "offer_type")] + public readonly ProbeEnum OfferType; + + public string PayloadData => _PayloadData; + } + + private enum ProbeEnum + { + [EnumMember(Value = "unknown")] + Unknown = 0, + + [EnumMember(Value = "win_back")] + WinBack, + } + + private class StrictResolver : DefaultContractResolver + { + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization serialization) + { + var property = base.CreateProperty(member, serialization); + if (property.Required == Required.AllowNull) + { + property.Required = Required.Always; + } + return property; + } + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + private static void Run() + { + var settings = new JsonSerializerSettings + { + ContractResolver = new StrictResolver(), + NullValueHandling = NullValueHandling.Ignore, + DateParseHandling = DateParseHandling.None, + Converters = { new Newtonsoft.Json.Converters.StringEnumConverter() }, + }; + + Report("scripting-backend", Application.platform + " il2cpp=" + IsIl2Cpp()); + + Check("readonly-fields", () => + { + var probe = JsonConvert.DeserializeObject( + "{\"flow_id\":\"f-1\",\"payload_data\":\"{}\",\"count\":7,\"offer_type\":\"win_back\"}", + settings + ); + return "public-readonly=" + (probe.InstanceIdentity ?? "") + + " private-readonly=" + (probe.PayloadData ?? "") + + " readonly-int=" + probe.Count + + " readonly-enum=" + probe.OfferType; + }); + + Check("required-missing", () => + { + JsonConvert.DeserializeObject("{\"count\":1}", settings); + return "no exception (UNEXPECTED)"; + }); + + Check("required-null", () => + { + JsonConvert.DeserializeObject("{\"flow_id\":null}", settings); + return "no exception (UNEXPECTED)"; + }); + + Check("enum-write", () => + JsonConvert.SerializeObject( + JsonConvert.DeserializeObject("{\"flow_id\":\"f\",\"offer_type\":\"win_back\"}", settings), + settings + ) + ); + + Check("dictionary-object", () => + { + var parsed = JsonConvert.DeserializeObject>( + "{\"n\":42,\"s\":\"x\",\"nested\":{\"k\":true}}", + settings + ); + return "n=" + parsed["n"].GetType().Name + " nested=" + parsed["nested"].GetType().Name; + }); + } + + private static bool IsIl2Cpp() + { +#if ENABLE_IL2CPP + return true; +#else + return false; +#endif + } + + private static void Check(string name, Func action) + { + try + { + Report(name, action()); + } + catch (Exception e) + { + Report(name, "threw " + e.GetType().Name + ": " + e.Message); + } + } + + private static void Report(string name, string result) => + Debug.Log("[AOT-PROBE] " + name + " -> " + result); +} diff --git a/tests/aot-probe/ProbeBuild.cs b/tests/aot-probe/ProbeBuild.cs new file mode 100644 index 0000000..e5e2531 --- /dev/null +++ b/tests/aot-probe/ProbeBuild.cs @@ -0,0 +1,38 @@ +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using UnityEngine; + +public static class ProbeBuild +{ + public static void BuildIOSSimulator() + { + PlayerSettings.SetScriptingBackend(NamedBuildTarget.iOS, ScriptingImplementation.IL2CPP); + PlayerSettings.SetManagedStrippingLevel(NamedBuildTarget.iOS, ManagedStrippingLevel.High); + PlayerSettings.iOS.sdkVersion = iOSSdkVersion.SimulatorSDK; + // Apple Silicon simulators are arm64; Unity defaults the simulator player to x86_64. + PlayerSettings.SetPropertyInt("iOSSimulatorArchitecture", 1, BuildTargetGroup.iOS); + PlayerSettings.iOS.targetOSVersionString = "15.0"; + PlayerSettings.applicationIdentifier = "io.adapty.aotprobe"; + PlayerSettings.productName = "AotProbe"; + + var scenePath = "Assets/Probe.unity"; + var scene = UnityEditor.SceneManagement.EditorSceneManager.NewScene( + UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, + UnityEditor.SceneManagement.NewSceneMode.Single + ); + UnityEditor.SceneManagement.EditorSceneManager.SaveScene(scene, scenePath); + + var report = BuildPipeline.BuildPlayer(new BuildPlayerOptions + { + scenes = new[] { scenePath }, + locationPathName = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ios-build"), + target = BuildTarget.iOS, + targetGroup = BuildTargetGroup.iOS, + options = BuildOptions.None, + }); + + Debug.Log("ProbeBuild: " + report.summary.result + ", errors: " + report.summary.totalErrors); + EditorApplication.Exit(report.summary.result == BuildResult.Succeeded ? 0 : 1); + } +} diff --git a/tests/aot-probe/README.md b/tests/aot-probe/README.md new file mode 100644 index 0000000..97dd87f --- /dev/null +++ b/tests/aot-probe/README.md @@ -0,0 +1,53 @@ +# IL2CPP serialization probe + +Checks on a real IL2CPP player what the Newtonsoft migration assumes from desktop behaviour. +Not part of the build; run by hand when one of those assumptions needs re-confirming. + +## Running + +The probe lives in a throwaway Unity project so it does not drag the SDK's native plugins into +the build: + +```bash +UNITY=/Applications/Unity/Hub/Editor/6000.4.5f1/Unity.app/Contents/MacOS/Unity +PROBE=/tmp/AotProbe + +"$UNITY" -batchmode -quit -createProject "$PROBE" +mkdir -p "$PROBE/Assets/Scripts" "$PROBE/Assets/Editor" +cp AotSerializationProbe.cs "$PROBE/Assets/Scripts/" +cp ProbeBuild.cs "$PROBE/Assets/Editor/" +printf '\n \n\n' > "$PROBE/Assets/link.xml" +# add "com.unity.nuget.newtonsoft-json": "3.2.2" to "$PROBE/Packages/manifest.json" + +./run-aot-probe.sh # adjust SP inside to point at the project +``` + +`run-aot-probe.sh` builds the player, swaps in the arm64 simulator runtime (Unity emits the +simulator player as x86_64 from the command line regardless of `iOSSimulatorArchitecture`), +builds the Xcode project, installs it on the booted simulator and prints the probe output. + +## Results, 01.08.2026 — Unity 6000.4.5f1, IL2CPP, stripping High + +``` +scripting-backend -> IPhonePlayer il2cpp=True +readonly-fields -> public-readonly=f-1 private-readonly={} readonly-int=7 readonly-enum=WinBack +required-missing -> threw JsonSerializationException: Required property 'flow_id' not found +required-null -> threw JsonSerializationException: Required property 'flow_id' expects a value but got null +enum-write -> {"flow_id":"f","count":0,"offer_type":"win_back"} +dictionary-object -> n=Int64 nested=JObject +``` + +What this settles: + +- **`readonly` fields are assigned under AOT** — public and private alike, including value types + and enums. Keeping the SDK's `readonly` fields as they are is safe. (The count this line used to + carry, 155, matched no reading of the tree even when it was written: `public readonly` under + `Runtime/` is 148 and every `readonly` field is 196. It bought nothing the sentence needs.) +- **`AdaptyContractResolver` works on IL2CPP**: a missing required field throws, and so does an + explicit null once `Required.AllowNull` is raised to `Required.Always`. +- **`[EnumMember]` names are used on write.** +- **`Dictionary` yields `Int64` and `JObject`**, same as on desktop — the SDK needs + its own converter to keep returning `double` and nested dictionaries. +- **`link.xml` is mandatory.** The first run, without it, failed every model case with + *"Unable to find a constructor to use for type Probe"* — stripping at High had removed the + constructor of a type only ever created by reflection. diff --git a/tests/aot-probe/run-aot-probe.sh b/tests/aot-probe/run-aot-probe.sh new file mode 100755 index 0000000..f71cc50 --- /dev/null +++ b/tests/aot-probe/run-aot-probe.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Rebuilds the IL2CPP probe for the arm64 simulator and prints its output. +# +# Unity generates the simulator player as x86_64 regardless of the architecture setting when +# driven from the command line, so the arm64 runtime and baselib are swapped in afterwards. + +set -euo pipefail + +SP="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT="$SP/AotProbe" +BUILD="$PROJECT/ios-build" +UNITY=/Applications/Unity/Hub/Editor/6000.4.5f1/Unity.app/Contents/MacOS/Unity +TRAMPOLINE=/Applications/Unity/Hub/Editor/6000.4.5f1/PlaybackEngines/iOSSupport/Trampoline +BUNDLE_ID=io.adapty.aotprobe + +echo "== unity player ==" +rm -rf "$BUILD" +"$UNITY" -batchmode -quit -projectPath "$PROJECT" \ + -executeMethod ProbeBuild.BuildIOSSimulator -logFile "$SP/aotprobe-build.log" >/dev/null 2>&1 +grep -E "ProbeBuild:" "$SP/aotprobe-build.log" | tail -1 + +echo "== arm64 swap ==" +rm -rf "$BUILD/Frameworks/UnityRuntime.framework" +cp -R "$TRAMPOLINE/Frameworks/UnityRuntime-sim-arm64/UnityRuntime.framework" "$BUILD/Frameworks/" +cp "$TRAMPOLINE/Libraries/baselib-sim-arm64.a" "$BUILD/Libraries/baselib.a" + +echo "== xcodebuild ==" +cd "$BUILD" +rm -rf dd +xcodebuild -project Unity-iPhone.xcodeproj -scheme Unity-iPhone -configuration Debug \ + -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath ./dd CODE_SIGNING_ALLOWED=NO ARCHS=arm64 ONLY_ACTIVE_ARCH=NO build \ + > "$BUILD/xcodebuild.log" 2>&1 +grep -E "BUILD SUCCEEDED|BUILD FAILED" "$BUILD/xcodebuild.log" | tail -1 + +echo "== run ==" +APP="$BUILD/dd/Build/Products/Debug-iphonesimulator/AotProbe.app" +xcrun simctl terminate booted "$BUNDLE_ID" >/dev/null 2>&1 || true +xcrun simctl uninstall booted "$BUNDLE_ID" >/dev/null 2>&1 || true +xcrun simctl install booted "$APP" +(xcrun simctl launch --console-pty booted "$BUNDLE_ID" > /tmp/aot-console.txt 2>&1 &) +sleep 15 +pkill -f "simctl launch" >/dev/null 2>&1 || true +xcrun simctl terminate booted "$BUNDLE_ID" >/dev/null 2>&1 || true + +echo "== probe output ==" +grep -o "\[AOT-PROBE\].*" /tmp/aot-console.txt || echo "(no probe output captured)" diff --git a/tests/shared/Fixtures/approved/error-full.android.approved.txt b/tests/shared/Fixtures/approved/error-full.android.approved.txt new file mode 100644 index 0000000..d48e6f4 --- /dev/null +++ b/tests/shared/Fixtures/approved/error-full.android.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyError", + "Code": "BadRequest (2003)", + "Detail": "AdaptyError.productNotFound(vendorProductId: \"com.adapty.sample.monthly\")", + "Message": "The product was not found" +} diff --git a/tests/shared/Fixtures/approved/error-full.editor.approved.txt b/tests/shared/Fixtures/approved/error-full.editor.approved.txt new file mode 100644 index 0000000..d48e6f4 --- /dev/null +++ b/tests/shared/Fixtures/approved/error-full.editor.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyError", + "Code": "BadRequest (2003)", + "Detail": "AdaptyError.productNotFound(vendorProductId: \"com.adapty.sample.monthly\")", + "Message": "The product was not found" +} diff --git a/tests/shared/Fixtures/approved/error-full.ios.approved.txt b/tests/shared/Fixtures/approved/error-full.ios.approved.txt new file mode 100644 index 0000000..d48e6f4 --- /dev/null +++ b/tests/shared/Fixtures/approved/error-full.ios.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyError", + "Code": "BadRequest (2003)", + "Detail": "AdaptyError.productNotFound(vendorProductId: \"com.adapty.sample.monthly\")", + "Message": "The product was not found" +} diff --git a/tests/shared/Fixtures/approved/error-minimal.android.approved.txt b/tests/shared/Fixtures/approved/error-minimal.android.approved.txt new file mode 100644 index 0000000..86ad00f --- /dev/null +++ b/tests/shared/Fixtures/approved/error-minimal.android.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyError", + "Code": "Unknown (0)", + "Detail": null, + "Message": "Unknown error" +} diff --git a/tests/shared/Fixtures/approved/error-minimal.editor.approved.txt b/tests/shared/Fixtures/approved/error-minimal.editor.approved.txt new file mode 100644 index 0000000..86ad00f --- /dev/null +++ b/tests/shared/Fixtures/approved/error-minimal.editor.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyError", + "Code": "Unknown (0)", + "Detail": null, + "Message": "Unknown error" +} diff --git a/tests/shared/Fixtures/approved/error-minimal.ios.approved.txt b/tests/shared/Fixtures/approved/error-minimal.ios.approved.txt new file mode 100644 index 0000000..86ad00f --- /dev/null +++ b/tests/shared/Fixtures/approved/error-minimal.ios.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyError", + "Code": "Unknown (0)", + "Detail": null, + "Message": "Unknown error" +} diff --git a/tests/shared/Fixtures/approved/flow-full-remote-config-dictionary.android.approved.txt b/tests/shared/Fixtures/approved/flow-full-remote-config-dictionary.android.approved.txt new file mode 100644 index 0000000..dc6fddd --- /dev/null +++ b/tests/shared/Fixtures/approved/flow-full-remote-config-dictionary.android.approved.txt @@ -0,0 +1,22 @@ +{ + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" +} diff --git a/tests/shared/Fixtures/approved/flow-full-remote-config-dictionary.editor.approved.txt b/tests/shared/Fixtures/approved/flow-full-remote-config-dictionary.editor.approved.txt new file mode 100644 index 0000000..dc6fddd --- /dev/null +++ b/tests/shared/Fixtures/approved/flow-full-remote-config-dictionary.editor.approved.txt @@ -0,0 +1,22 @@ +{ + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" +} diff --git a/tests/shared/Fixtures/approved/flow-full-remote-config-dictionary.ios.approved.txt b/tests/shared/Fixtures/approved/flow-full-remote-config-dictionary.ios.approved.txt new file mode 100644 index 0000000..dc6fddd --- /dev/null +++ b/tests/shared/Fixtures/approved/flow-full-remote-config-dictionary.ios.approved.txt @@ -0,0 +1,22 @@ +{ + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" +} diff --git a/tests/shared/Fixtures/approved/flow-full.android.approved.txt b/tests/shared/Fixtures/approved/flow-full.android.approved.txt new file mode 100644 index 0000000..72c2346 --- /dev/null +++ b/tests/shared/Fixtures/approved/flow-full.android.approved.txt @@ -0,0 +1,261 @@ +{ + "$type": "AdaptyFlow", + "FlowVersionId": "flow-version-0001", + "InstanceIdentity": "flow-0001", + "Name": "Winter Flow", + "Paywalls (property)": [ + { + "$type": "AdaptyFlowPaywall", + "InstanceIdentity": "paywall-0001", + "Name": "Winter Paywall", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "winter_test", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": true, + "PlacementAudienceVersionId": "pav-0001", + "Revision": 7 + }, + "ProductIdentifiers (property)": [ + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": "base-plan-1", + "VendorProductId": "com.adapty.sample.monthly", + "_AdaptyProductId": "adapty-product-1" + }, + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.yearly", + "_AdaptyProductId": "adapty-product-2" + } + ], + "VariationId": "variation-0001", + "VendorProductIds (property)": [ + "com.adapty.sample.monthly", + "com.adapty.sample.yearly" + ], + "_Products": [ + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-1", + "AndroidBasePlanId": "base-plan-1", + "AndroidOfferId": "offer-1", + "FlowProductId": "flow-product-1", + "ProductType": "subscription", + "PromotionalOfferId": null, + "VendorProductId": "com.adapty.sample.monthly", + "WinBackOfferId": null + }, + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-2", + "AndroidBasePlanId": null, + "AndroidOfferId": null, + "FlowProductId": null, + "ProductType": "subscription", + "PromotionalOfferId": null, + "VendorProductId": "com.adapty.sample.yearly", + "WinBackOfferId": null + } + ], + "_WebPurchaseUrl": "https://pay.adapty.io/checkout/abc" + } + ], + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "winter_test", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": true, + "PlacementAudienceVersionId": "pav-0001", + "Revision": 7 + }, + "ProductIdentifiers (property)": [ + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": "base-plan-1", + "VendorProductId": "com.adapty.sample.monthly", + "_AdaptyProductId": "adapty-product-1" + }, + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.yearly", + "_AdaptyProductId": "adapty-product-2" + } + ], + "RemoteConfig (property)": { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "Dictionary (property)": { + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" + }, + "Locale": "en" + }, + "RemoteConfigs (property)": [ + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "Dictionary (property)": { + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" + }, + "Locale": "en" + }, + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\":\"Hazte premium\"}", + "Dictionary (property)": { + "title": "Hazte premium" + }, + "Locale": "es" + } + ], + "VariationId": "variation-0001", + "VendorProductIds (property)": [ + "com.adapty.sample.monthly", + "com.adapty.sample.yearly" + ], + "_PayloadData": "{\"custom\":\"payload\"}", + "_Paywalls": [ + { + "$type": "AdaptyFlowPaywall", + "InstanceIdentity": "paywall-0001", + "Name": "Winter Paywall", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "winter_test", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": true, + "PlacementAudienceVersionId": "pav-0001", + "Revision": 7 + }, + "ProductIdentifiers (property)": [ + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": "base-plan-1", + "VendorProductId": "com.adapty.sample.monthly", + "_AdaptyProductId": "adapty-product-1" + }, + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.yearly", + "_AdaptyProductId": "adapty-product-2" + } + ], + "VariationId": "variation-0001", + "VendorProductIds (property)": [ + "com.adapty.sample.monthly", + "com.adapty.sample.yearly" + ], + "_Products": [ + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-1", + "AndroidBasePlanId": "base-plan-1", + "AndroidOfferId": "offer-1", + "FlowProductId": "flow-product-1", + "ProductType": "subscription", + "PromotionalOfferId": null, + "VendorProductId": "com.adapty.sample.monthly", + "WinBackOfferId": null + }, + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-2", + "AndroidBasePlanId": null, + "AndroidOfferId": null, + "FlowProductId": null, + "ProductType": "subscription", + "PromotionalOfferId": null, + "VendorProductId": "com.adapty.sample.yearly", + "WinBackOfferId": null + } + ], + "_WebPurchaseUrl": "https://pay.adapty.io/checkout/abc" + } + ], + "_RemoteConfigs": [ + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "Dictionary (property)": { + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" + }, + "Locale": "en" + }, + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\":\"Hazte premium\"}", + "Dictionary (property)": { + "title": "Hazte premium" + }, + "Locale": "es" + } + ], + "_ResponseCreatedAt": 1753876800000 +} diff --git a/tests/shared/Fixtures/approved/flow-full.editor.approved.txt b/tests/shared/Fixtures/approved/flow-full.editor.approved.txt new file mode 100644 index 0000000..4a59173 --- /dev/null +++ b/tests/shared/Fixtures/approved/flow-full.editor.approved.txt @@ -0,0 +1,261 @@ +{ + "$type": "AdaptyFlow", + "FlowVersionId": "flow-version-0001", + "InstanceIdentity": "flow-0001", + "Name": "Winter Flow", + "Paywalls (property)": [ + { + "$type": "AdaptyFlowPaywall", + "InstanceIdentity": "paywall-0001", + "Name": "Winter Paywall", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "winter_test", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": true, + "PlacementAudienceVersionId": "pav-0001", + "Revision": 7 + }, + "ProductIdentifiers (property)": [ + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.monthly", + "_AdaptyProductId": "adapty-product-1" + }, + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.yearly", + "_AdaptyProductId": "adapty-product-2" + } + ], + "VariationId": "variation-0001", + "VendorProductIds (property)": [ + "com.adapty.sample.monthly", + "com.adapty.sample.yearly" + ], + "_Products": [ + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-1", + "AndroidBasePlanId": null, + "AndroidOfferId": null, + "FlowProductId": "flow-product-1", + "ProductType": "subscription", + "PromotionalOfferId": null, + "VendorProductId": "com.adapty.sample.monthly", + "WinBackOfferId": null + }, + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-2", + "AndroidBasePlanId": null, + "AndroidOfferId": null, + "FlowProductId": null, + "ProductType": "subscription", + "PromotionalOfferId": null, + "VendorProductId": "com.adapty.sample.yearly", + "WinBackOfferId": null + } + ], + "_WebPurchaseUrl": "https://pay.adapty.io/checkout/abc" + } + ], + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "winter_test", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": true, + "PlacementAudienceVersionId": "pav-0001", + "Revision": 7 + }, + "ProductIdentifiers (property)": [ + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.monthly", + "_AdaptyProductId": "adapty-product-1" + }, + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.yearly", + "_AdaptyProductId": "adapty-product-2" + } + ], + "RemoteConfig (property)": { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "Dictionary (property)": { + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" + }, + "Locale": "en" + }, + "RemoteConfigs (property)": [ + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "Dictionary (property)": { + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" + }, + "Locale": "en" + }, + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\":\"Hazte premium\"}", + "Dictionary (property)": { + "title": "Hazte premium" + }, + "Locale": "es" + } + ], + "VariationId": "variation-0001", + "VendorProductIds (property)": [ + "com.adapty.sample.monthly", + "com.adapty.sample.yearly" + ], + "_PayloadData": "{\"custom\":\"payload\"}", + "_Paywalls": [ + { + "$type": "AdaptyFlowPaywall", + "InstanceIdentity": "paywall-0001", + "Name": "Winter Paywall", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "winter_test", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": true, + "PlacementAudienceVersionId": "pav-0001", + "Revision": 7 + }, + "ProductIdentifiers (property)": [ + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.monthly", + "_AdaptyProductId": "adapty-product-1" + }, + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.yearly", + "_AdaptyProductId": "adapty-product-2" + } + ], + "VariationId": "variation-0001", + "VendorProductIds (property)": [ + "com.adapty.sample.monthly", + "com.adapty.sample.yearly" + ], + "_Products": [ + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-1", + "AndroidBasePlanId": null, + "AndroidOfferId": null, + "FlowProductId": "flow-product-1", + "ProductType": "subscription", + "PromotionalOfferId": null, + "VendorProductId": "com.adapty.sample.monthly", + "WinBackOfferId": null + }, + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-2", + "AndroidBasePlanId": null, + "AndroidOfferId": null, + "FlowProductId": null, + "ProductType": "subscription", + "PromotionalOfferId": null, + "VendorProductId": "com.adapty.sample.yearly", + "WinBackOfferId": null + } + ], + "_WebPurchaseUrl": "https://pay.adapty.io/checkout/abc" + } + ], + "_RemoteConfigs": [ + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "Dictionary (property)": { + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" + }, + "Locale": "en" + }, + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\":\"Hazte premium\"}", + "Dictionary (property)": { + "title": "Hazte premium" + }, + "Locale": "es" + } + ], + "_ResponseCreatedAt": 1753876800000 +} diff --git a/tests/shared/Fixtures/approved/flow-full.ios.approved.txt b/tests/shared/Fixtures/approved/flow-full.ios.approved.txt new file mode 100644 index 0000000..08b0fc5 --- /dev/null +++ b/tests/shared/Fixtures/approved/flow-full.ios.approved.txt @@ -0,0 +1,261 @@ +{ + "$type": "AdaptyFlow", + "FlowVersionId": "flow-version-0001", + "InstanceIdentity": "flow-0001", + "Name": "Winter Flow", + "Paywalls (property)": [ + { + "$type": "AdaptyFlowPaywall", + "InstanceIdentity": "paywall-0001", + "Name": "Winter Paywall", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "winter_test", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": true, + "PlacementAudienceVersionId": "pav-0001", + "Revision": 7 + }, + "ProductIdentifiers (property)": [ + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.monthly", + "_AdaptyProductId": "adapty-product-1" + }, + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.yearly", + "_AdaptyProductId": "adapty-product-2" + } + ], + "VariationId": "variation-0001", + "VendorProductIds (property)": [ + "com.adapty.sample.monthly", + "com.adapty.sample.yearly" + ], + "_Products": [ + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-1", + "AndroidBasePlanId": null, + "AndroidOfferId": null, + "FlowProductId": "flow-product-1", + "ProductType": "subscription", + "PromotionalOfferId": "promo-1", + "VendorProductId": "com.adapty.sample.monthly", + "WinBackOfferId": "winback-1" + }, + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-2", + "AndroidBasePlanId": null, + "AndroidOfferId": null, + "FlowProductId": null, + "ProductType": "subscription", + "PromotionalOfferId": null, + "VendorProductId": "com.adapty.sample.yearly", + "WinBackOfferId": null + } + ], + "_WebPurchaseUrl": "https://pay.adapty.io/checkout/abc" + } + ], + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "winter_test", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": true, + "PlacementAudienceVersionId": "pav-0001", + "Revision": 7 + }, + "ProductIdentifiers (property)": [ + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.monthly", + "_AdaptyProductId": "adapty-product-1" + }, + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.yearly", + "_AdaptyProductId": "adapty-product-2" + } + ], + "RemoteConfig (property)": { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "Dictionary (property)": { + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" + }, + "Locale": "en" + }, + "RemoteConfigs (property)": [ + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "Dictionary (property)": { + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" + }, + "Locale": "en" + }, + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\":\"Hazte premium\"}", + "Dictionary (property)": { + "title": "Hazte premium" + }, + "Locale": "es" + } + ], + "VariationId": "variation-0001", + "VendorProductIds (property)": [ + "com.adapty.sample.monthly", + "com.adapty.sample.yearly" + ], + "_PayloadData": "{\"custom\":\"payload\"}", + "_Paywalls": [ + { + "$type": "AdaptyFlowPaywall", + "InstanceIdentity": "paywall-0001", + "Name": "Winter Paywall", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "winter_test", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": true, + "PlacementAudienceVersionId": "pav-0001", + "Revision": 7 + }, + "ProductIdentifiers (property)": [ + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.monthly", + "_AdaptyProductId": "adapty-product-1" + }, + { + "$type": "AdaptyProductIdentifier", + "BasePlanId": null, + "VendorProductId": "com.adapty.sample.yearly", + "_AdaptyProductId": "adapty-product-2" + } + ], + "VariationId": "variation-0001", + "VendorProductIds (property)": [ + "com.adapty.sample.monthly", + "com.adapty.sample.yearly" + ], + "_Products": [ + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-1", + "AndroidBasePlanId": null, + "AndroidOfferId": null, + "FlowProductId": "flow-product-1", + "ProductType": "subscription", + "PromotionalOfferId": "promo-1", + "VendorProductId": "com.adapty.sample.monthly", + "WinBackOfferId": "winback-1" + }, + { + "$type": "ProductReference", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-2", + "AndroidBasePlanId": null, + "AndroidOfferId": null, + "FlowProductId": null, + "ProductType": "subscription", + "PromotionalOfferId": null, + "VendorProductId": "com.adapty.sample.yearly", + "WinBackOfferId": null + } + ], + "_WebPurchaseUrl": "https://pay.adapty.io/checkout/abc" + } + ], + "_RemoteConfigs": [ + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "Dictionary (property)": { + "absent": null, + "discount": 30, + "enabled": true, + "mixed": [ + 1, + true, + null, + { + "k": "v" + } + ], + "nested": { + "k": "v" + }, + "released_at": "2026-07-30T10:00:00.000Z", + "tags": [ + "a", + "b" + ], + "title": "Go premium" + }, + "Locale": "en" + }, + { + "$type": "AdaptyRemoteConfig", + "Data": "{\"title\":\"Hazte premium\"}", + "Dictionary (property)": { + "title": "Hazte premium" + }, + "Locale": "es" + } + ], + "_ResponseCreatedAt": 1753876800000 +} diff --git a/tests/shared/Fixtures/approved/flow-minimal.android.approved.txt b/tests/shared/Fixtures/approved/flow-minimal.android.approved.txt new file mode 100644 index 0000000..1090e2b --- /dev/null +++ b/tests/shared/Fixtures/approved/flow-minimal.android.approved.txt @@ -0,0 +1,25 @@ +{ + "$type": "AdaptyFlow", + "FlowVersionId": null, + "InstanceIdentity": "flow-0002", + "Name": "Minimal Flow", + "Paywalls (property)": [], + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "default", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": false, + "PlacementAudienceVersionId": "pav-0002", + "Revision": 1 + }, + "ProductIdentifiers (property)": [], + "RemoteConfig (property)": null, + "RemoteConfigs (property)": [], + "VariationId": "variation-0002", + "VendorProductIds (property)": [], + "_PayloadData": null, + "_Paywalls": [], + "_RemoteConfigs": [], + "_ResponseCreatedAt": 1753876800000 +} diff --git a/tests/shared/Fixtures/approved/flow-minimal.editor.approved.txt b/tests/shared/Fixtures/approved/flow-minimal.editor.approved.txt new file mode 100644 index 0000000..1090e2b --- /dev/null +++ b/tests/shared/Fixtures/approved/flow-minimal.editor.approved.txt @@ -0,0 +1,25 @@ +{ + "$type": "AdaptyFlow", + "FlowVersionId": null, + "InstanceIdentity": "flow-0002", + "Name": "Minimal Flow", + "Paywalls (property)": [], + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "default", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": false, + "PlacementAudienceVersionId": "pav-0002", + "Revision": 1 + }, + "ProductIdentifiers (property)": [], + "RemoteConfig (property)": null, + "RemoteConfigs (property)": [], + "VariationId": "variation-0002", + "VendorProductIds (property)": [], + "_PayloadData": null, + "_Paywalls": [], + "_RemoteConfigs": [], + "_ResponseCreatedAt": 1753876800000 +} diff --git a/tests/shared/Fixtures/approved/flow-minimal.ios.approved.txt b/tests/shared/Fixtures/approved/flow-minimal.ios.approved.txt new file mode 100644 index 0000000..1090e2b --- /dev/null +++ b/tests/shared/Fixtures/approved/flow-minimal.ios.approved.txt @@ -0,0 +1,25 @@ +{ + "$type": "AdaptyFlow", + "FlowVersionId": null, + "InstanceIdentity": "flow-0002", + "Name": "Minimal Flow", + "Paywalls (property)": [], + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "default", + "AudienceName": "All Users", + "Id": "onboarding", + "IsTrackingPurchases": false, + "PlacementAudienceVersionId": "pav-0002", + "Revision": 1 + }, + "ProductIdentifiers (property)": [], + "RemoteConfig (property)": null, + "RemoteConfigs (property)": [], + "VariationId": "variation-0002", + "VendorProductIds (property)": [], + "_PayloadData": null, + "_Paywalls": [], + "_RemoteConfigs": [], + "_ResponseCreatedAt": 1753876800000 +} diff --git a/tests/shared/Fixtures/approved/installation-determined-minimal.android.approved.txt b/tests/shared/Fixtures/approved/installation-determined-minimal.android.approved.txt new file mode 100644 index 0000000..29cc400 --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-determined-minimal.android.approved.txt @@ -0,0 +1,11 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": { + "$type": "AdaptyInstallationDetails", + "AppLaunchCount": 1, + "InstallId": null, + "InstallTime": { "utc": "2026-07-30T10:00:00.0000000Z", "kind": "Local" }, + "Payload": null + }, + "Status": "Determined (2)" +} diff --git a/tests/shared/Fixtures/approved/installation-determined-minimal.editor.approved.txt b/tests/shared/Fixtures/approved/installation-determined-minimal.editor.approved.txt new file mode 100644 index 0000000..29cc400 --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-determined-minimal.editor.approved.txt @@ -0,0 +1,11 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": { + "$type": "AdaptyInstallationDetails", + "AppLaunchCount": 1, + "InstallId": null, + "InstallTime": { "utc": "2026-07-30T10:00:00.0000000Z", "kind": "Local" }, + "Payload": null + }, + "Status": "Determined (2)" +} diff --git a/tests/shared/Fixtures/approved/installation-determined-minimal.ios.approved.txt b/tests/shared/Fixtures/approved/installation-determined-minimal.ios.approved.txt new file mode 100644 index 0000000..29cc400 --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-determined-minimal.ios.approved.txt @@ -0,0 +1,11 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": { + "$type": "AdaptyInstallationDetails", + "AppLaunchCount": 1, + "InstallId": null, + "InstallTime": { "utc": "2026-07-30T10:00:00.0000000Z", "kind": "Local" }, + "Payload": null + }, + "Status": "Determined (2)" +} diff --git a/tests/shared/Fixtures/approved/installation-determined.android.approved.txt b/tests/shared/Fixtures/approved/installation-determined.android.approved.txt new file mode 100644 index 0000000..91ab7db --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-determined.android.approved.txt @@ -0,0 +1,11 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": { + "$type": "AdaptyInstallationDetails", + "AppLaunchCount": 7, + "InstallId": "install-0001", + "InstallTime": { "utc": "2026-07-30T10:00:00.0000000Z", "kind": "Local" }, + "Payload": "{\"campaign\":\"summer\"}" + }, + "Status": "Determined (2)" +} diff --git a/tests/shared/Fixtures/approved/installation-determined.editor.approved.txt b/tests/shared/Fixtures/approved/installation-determined.editor.approved.txt new file mode 100644 index 0000000..91ab7db --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-determined.editor.approved.txt @@ -0,0 +1,11 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": { + "$type": "AdaptyInstallationDetails", + "AppLaunchCount": 7, + "InstallId": "install-0001", + "InstallTime": { "utc": "2026-07-30T10:00:00.0000000Z", "kind": "Local" }, + "Payload": "{\"campaign\":\"summer\"}" + }, + "Status": "Determined (2)" +} diff --git a/tests/shared/Fixtures/approved/installation-determined.ios.approved.txt b/tests/shared/Fixtures/approved/installation-determined.ios.approved.txt new file mode 100644 index 0000000..91ab7db --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-determined.ios.approved.txt @@ -0,0 +1,11 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": { + "$type": "AdaptyInstallationDetails", + "AppLaunchCount": 7, + "InstallId": "install-0001", + "InstallTime": { "utc": "2026-07-30T10:00:00.0000000Z", "kind": "Local" }, + "Payload": "{\"campaign\":\"summer\"}" + }, + "Status": "Determined (2)" +} diff --git a/tests/shared/Fixtures/approved/installation-not-available.android.approved.txt b/tests/shared/Fixtures/approved/installation-not-available.android.approved.txt new file mode 100644 index 0000000..30ddb60 --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-not-available.android.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": null, + "Status": "NotAvailable (0)" +} diff --git a/tests/shared/Fixtures/approved/installation-not-available.editor.approved.txt b/tests/shared/Fixtures/approved/installation-not-available.editor.approved.txt new file mode 100644 index 0000000..30ddb60 --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-not-available.editor.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": null, + "Status": "NotAvailable (0)" +} diff --git a/tests/shared/Fixtures/approved/installation-not-available.ios.approved.txt b/tests/shared/Fixtures/approved/installation-not-available.ios.approved.txt new file mode 100644 index 0000000..30ddb60 --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-not-available.ios.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": null, + "Status": "NotAvailable (0)" +} diff --git a/tests/shared/Fixtures/approved/installation-not-determined.android.approved.txt b/tests/shared/Fixtures/approved/installation-not-determined.android.approved.txt new file mode 100644 index 0000000..aa2d9b3 --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-not-determined.android.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": null, + "Status": "NotDetermined (1)" +} diff --git a/tests/shared/Fixtures/approved/installation-not-determined.editor.approved.txt b/tests/shared/Fixtures/approved/installation-not-determined.editor.approved.txt new file mode 100644 index 0000000..aa2d9b3 --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-not-determined.editor.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": null, + "Status": "NotDetermined (1)" +} diff --git a/tests/shared/Fixtures/approved/installation-not-determined.ios.approved.txt b/tests/shared/Fixtures/approved/installation-not-determined.ios.approved.txt new file mode 100644 index 0000000..aa2d9b3 --- /dev/null +++ b/tests/shared/Fixtures/approved/installation-not-determined.ios.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyInstallationStatus", + "Details (property)": null, + "Status": "NotDetermined (1)" +} diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed-bare.android.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed-bare.android.approved.txt new file mode 100644 index 0000000..ee772c3 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed-bare.android.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyOnboardingsAnalyticsEventScreenCompleted", + "ElementId": null, + "Reply": null +} diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed-bare.editor.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed-bare.editor.approved.txt new file mode 100644 index 0000000..ee772c3 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed-bare.editor.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyOnboardingsAnalyticsEventScreenCompleted", + "ElementId": null, + "Reply": null +} diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed-bare.ios.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed-bare.ios.approved.txt new file mode 100644 index 0000000..ee772c3 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed-bare.ios.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyOnboardingsAnalyticsEventScreenCompleted", + "ElementId": null, + "Reply": null +} diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed.android.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed.android.approved.txt new file mode 100644 index 0000000..007b432 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed.android.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyOnboardingsAnalyticsEventScreenCompleted", + "ElementId": "gender_select", + "Reply": "female" +} diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed.editor.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed.editor.approved.txt new file mode 100644 index 0000000..007b432 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed.editor.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyOnboardingsAnalyticsEventScreenCompleted", + "ElementId": "gender_select", + "Reply": "female" +} diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed.ios.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed.ios.approved.txt new file mode 100644 index 0000000..007b432 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-screen-completed.ios.approved.txt @@ -0,0 +1,5 @@ +{ + "$type": "AdaptyOnboardingsAnalyticsEventScreenCompleted", + "ElementId": "gender_select", + "Reply": "female" +} diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-started.android.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-started.android.approved.txt new file mode 100644 index 0000000..cff7e53 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-started.android.approved.txt @@ -0,0 +1 @@ +"AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingStarted" diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-started.editor.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-started.editor.approved.txt new file mode 100644 index 0000000..cff7e53 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-started.editor.approved.txt @@ -0,0 +1 @@ +"AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingStarted" diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-started.ios.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-started.ios.approved.txt new file mode 100644 index 0000000..cff7e53 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-started.ios.approved.txt @@ -0,0 +1 @@ +"AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingStarted" diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-unknown.android.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-unknown.android.approved.txt new file mode 100644 index 0000000..93aa386 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-unknown.android.approved.txt @@ -0,0 +1,4 @@ +{ + "$type": "AdaptyOnboardingsAnalyticsEventUnknown", + "Name": "paywall_screen_presented" +} diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-unknown.editor.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-unknown.editor.approved.txt new file mode 100644 index 0000000..93aa386 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-unknown.editor.approved.txt @@ -0,0 +1,4 @@ +{ + "$type": "AdaptyOnboardingsAnalyticsEventUnknown", + "Name": "paywall_screen_presented" +} diff --git a/tests/shared/Fixtures/approved/onboarding-analytics-unknown.ios.approved.txt b/tests/shared/Fixtures/approved/onboarding-analytics-unknown.ios.approved.txt new file mode 100644 index 0000000..93aa386 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-analytics-unknown.ios.approved.txt @@ -0,0 +1,4 @@ +{ + "$type": "AdaptyOnboardingsAnalyticsEventUnknown", + "Name": "paywall_screen_presented" +} diff --git a/tests/shared/Fixtures/approved/onboarding-date-picker-full.android.approved.txt b/tests/shared/Fixtures/approved/onboarding-date-picker-full.android.approved.txt new file mode 100644 index 0000000..5bb98ac --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-date-picker-full.android.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyOnboardingsDatePickerParams", + "Day": 30, + "Month": 7, + "Year": 2026 +} diff --git a/tests/shared/Fixtures/approved/onboarding-date-picker-full.editor.approved.txt b/tests/shared/Fixtures/approved/onboarding-date-picker-full.editor.approved.txt new file mode 100644 index 0000000..5bb98ac --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-date-picker-full.editor.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyOnboardingsDatePickerParams", + "Day": 30, + "Month": 7, + "Year": 2026 +} diff --git a/tests/shared/Fixtures/approved/onboarding-date-picker-full.ios.approved.txt b/tests/shared/Fixtures/approved/onboarding-date-picker-full.ios.approved.txt new file mode 100644 index 0000000..5bb98ac --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-date-picker-full.ios.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyOnboardingsDatePickerParams", + "Day": 30, + "Month": 7, + "Year": 2026 +} diff --git a/tests/shared/Fixtures/approved/onboarding-date-picker-partial.android.approved.txt b/tests/shared/Fixtures/approved/onboarding-date-picker-partial.android.approved.txt new file mode 100644 index 0000000..2f2b8e5 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-date-picker-partial.android.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyOnboardingsDatePickerParams", + "Day": null, + "Month": null, + "Year": 2026 +} diff --git a/tests/shared/Fixtures/approved/onboarding-date-picker-partial.editor.approved.txt b/tests/shared/Fixtures/approved/onboarding-date-picker-partial.editor.approved.txt new file mode 100644 index 0000000..2f2b8e5 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-date-picker-partial.editor.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyOnboardingsDatePickerParams", + "Day": null, + "Month": null, + "Year": 2026 +} diff --git a/tests/shared/Fixtures/approved/onboarding-date-picker-partial.ios.approved.txt b/tests/shared/Fixtures/approved/onboarding-date-picker-partial.ios.approved.txt new file mode 100644 index 0000000..2f2b8e5 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-date-picker-partial.ios.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyOnboardingsDatePickerParams", + "Day": null, + "Month": null, + "Year": 2026 +} diff --git a/tests/shared/Fixtures/approved/onboarding-full.android.approved.txt b/tests/shared/Fixtures/approved/onboarding-full.android.approved.txt new file mode 100644 index 0000000..9c799f9 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-full.android.approved.txt @@ -0,0 +1,31 @@ +{ + "$type": "AdaptyOnboarding", + "Name": "Welcome Onboarding", + "OnboardingId": "onboarding-0001", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "onboarding_test", + "AudienceName": "All Users", + "Id": "welcome", + "IsTrackingPurchases": false, + "PlacementAudienceVersionId": "pav-0002", + "Revision": 3 + }, + "RemoteConfig": { + "$type": "AdaptyRemoteConfig", + "Data": "{\"steps\":3,\"skippable\":false}", + "Dictionary (property)": { + "skippable": false, + "steps": 3 + }, + "Locale": "en" + }, + "VariationId": "variation-0002", + "_Builder": { + "$type": "OnboardingBuilder", + "ConfigUrl": "https://cdn.adapty.io/onboardings/onboarding-0001.json" + }, + "_PayloadData": "{\"custom\":\"onboarding payload\"}", + "_RequestLocale": "en", + "_ResponseCreatedAt": 1754006400 +} diff --git a/tests/shared/Fixtures/approved/onboarding-full.editor.approved.txt b/tests/shared/Fixtures/approved/onboarding-full.editor.approved.txt new file mode 100644 index 0000000..9c799f9 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-full.editor.approved.txt @@ -0,0 +1,31 @@ +{ + "$type": "AdaptyOnboarding", + "Name": "Welcome Onboarding", + "OnboardingId": "onboarding-0001", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "onboarding_test", + "AudienceName": "All Users", + "Id": "welcome", + "IsTrackingPurchases": false, + "PlacementAudienceVersionId": "pav-0002", + "Revision": 3 + }, + "RemoteConfig": { + "$type": "AdaptyRemoteConfig", + "Data": "{\"steps\":3,\"skippable\":false}", + "Dictionary (property)": { + "skippable": false, + "steps": 3 + }, + "Locale": "en" + }, + "VariationId": "variation-0002", + "_Builder": { + "$type": "OnboardingBuilder", + "ConfigUrl": "https://cdn.adapty.io/onboardings/onboarding-0001.json" + }, + "_PayloadData": "{\"custom\":\"onboarding payload\"}", + "_RequestLocale": "en", + "_ResponseCreatedAt": 1754006400 +} diff --git a/tests/shared/Fixtures/approved/onboarding-full.ios.approved.txt b/tests/shared/Fixtures/approved/onboarding-full.ios.approved.txt new file mode 100644 index 0000000..9c799f9 --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-full.ios.approved.txt @@ -0,0 +1,31 @@ +{ + "$type": "AdaptyOnboarding", + "Name": "Welcome Onboarding", + "OnboardingId": "onboarding-0001", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "onboarding_test", + "AudienceName": "All Users", + "Id": "welcome", + "IsTrackingPurchases": false, + "PlacementAudienceVersionId": "pav-0002", + "Revision": 3 + }, + "RemoteConfig": { + "$type": "AdaptyRemoteConfig", + "Data": "{\"steps\":3,\"skippable\":false}", + "Dictionary (property)": { + "skippable": false, + "steps": 3 + }, + "Locale": "en" + }, + "VariationId": "variation-0002", + "_Builder": { + "$type": "OnboardingBuilder", + "ConfigUrl": "https://cdn.adapty.io/onboardings/onboarding-0001.json" + }, + "_PayloadData": "{\"custom\":\"onboarding payload\"}", + "_RequestLocale": "en", + "_ResponseCreatedAt": 1754006400 +} diff --git a/tests/shared/Fixtures/approved/onboarding-minimal.android.approved.txt b/tests/shared/Fixtures/approved/onboarding-minimal.android.approved.txt new file mode 100644 index 0000000..b35fc8e --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-minimal.android.approved.txt @@ -0,0 +1,23 @@ +{ + "$type": "AdaptyOnboarding", + "Name": "Short Onboarding", + "OnboardingId": "onboarding-0002", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "onboarding_test", + "AudienceName": "All Users", + "Id": "welcome", + "IsTrackingPurchases": false, + "PlacementAudienceVersionId": "pav-0003", + "Revision": 1 + }, + "RemoteConfig": null, + "VariationId": "variation-0003", + "_Builder": { + "$type": "OnboardingBuilder", + "ConfigUrl": "https://cdn.adapty.io/onboardings/onboarding-0002.json" + }, + "_PayloadData": null, + "_RequestLocale": "es", + "_ResponseCreatedAt": 1754006401 +} diff --git a/tests/shared/Fixtures/approved/onboarding-minimal.editor.approved.txt b/tests/shared/Fixtures/approved/onboarding-minimal.editor.approved.txt new file mode 100644 index 0000000..b35fc8e --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-minimal.editor.approved.txt @@ -0,0 +1,23 @@ +{ + "$type": "AdaptyOnboarding", + "Name": "Short Onboarding", + "OnboardingId": "onboarding-0002", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "onboarding_test", + "AudienceName": "All Users", + "Id": "welcome", + "IsTrackingPurchases": false, + "PlacementAudienceVersionId": "pav-0003", + "Revision": 1 + }, + "RemoteConfig": null, + "VariationId": "variation-0003", + "_Builder": { + "$type": "OnboardingBuilder", + "ConfigUrl": "https://cdn.adapty.io/onboardings/onboarding-0002.json" + }, + "_PayloadData": null, + "_RequestLocale": "es", + "_ResponseCreatedAt": 1754006401 +} diff --git a/tests/shared/Fixtures/approved/onboarding-minimal.ios.approved.txt b/tests/shared/Fixtures/approved/onboarding-minimal.ios.approved.txt new file mode 100644 index 0000000..b35fc8e --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-minimal.ios.approved.txt @@ -0,0 +1,23 @@ +{ + "$type": "AdaptyOnboarding", + "Name": "Short Onboarding", + "OnboardingId": "onboarding-0002", + "Placement": { + "$type": "AdaptyPlacement", + "ABTestName": "onboarding_test", + "AudienceName": "All Users", + "Id": "welcome", + "IsTrackingPurchases": false, + "PlacementAudienceVersionId": "pav-0003", + "Revision": 1 + }, + "RemoteConfig": null, + "VariationId": "variation-0003", + "_Builder": { + "$type": "OnboardingBuilder", + "ConfigUrl": "https://cdn.adapty.io/onboardings/onboarding-0002.json" + }, + "_PayloadData": null, + "_RequestLocale": "es", + "_ResponseCreatedAt": 1754006401 +} diff --git a/tests/shared/Fixtures/approved/onboarding-select.android.approved.txt b/tests/shared/Fixtures/approved/onboarding-select.android.approved.txt new file mode 100644 index 0000000..50918bb --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-select.android.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyOnboardingsSelectParams", + "Id": "plan-1", + "Label": "Monthly", + "Value": "monthly" +} diff --git a/tests/shared/Fixtures/approved/onboarding-select.editor.approved.txt b/tests/shared/Fixtures/approved/onboarding-select.editor.approved.txt new file mode 100644 index 0000000..50918bb --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-select.editor.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyOnboardingsSelectParams", + "Id": "plan-1", + "Label": "Monthly", + "Value": "monthly" +} diff --git a/tests/shared/Fixtures/approved/onboarding-select.ios.approved.txt b/tests/shared/Fixtures/approved/onboarding-select.ios.approved.txt new file mode 100644 index 0000000..50918bb --- /dev/null +++ b/tests/shared/Fixtures/approved/onboarding-select.ios.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyOnboardingsSelectParams", + "Id": "plan-1", + "Label": "Monthly", + "Value": "monthly" +} diff --git a/tests/shared/Fixtures/approved/products-full.android.approved.txt b/tests/shared/Fixtures/approved/products-full.android.approved.txt new file mode 100644 index 0000000..4ae4afd --- /dev/null +++ b/tests/shared/Fixtures/approved/products-full.android.approved.txt @@ -0,0 +1,95 @@ +[ + { + "$type": "AdaptyPaywallProduct", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-1", + "FlowProductId": "flow-product-1", + "IsFamilyShareable": false, + "LocalizedDescription": "Full access, billed monthly", + "LocalizedTitle": "Monthly", + "PaywallABTestName": "winter_test", + "PaywallName": "Winter Paywall", + "PaywallProductIndex": 0, + "PaywallVariationId": "variation-0001", + "Price": { + "$type": "AdaptyPrice", + "Amount": 9.99, + "CurrencyCode": "USD", + "CurrencySymbol": "$", + "LocalizedString": "$9.99" + }, + "ProductType": "subscription", + "RegionCode": "US", + "Subscription": { + "$type": "AdaptySubscription", + "BasePlanId": "base-plan-1", + "GroupIdentifier": null, + "LocalizedPeriod": "1 month", + "Offer": { + "$type": "AdaptySubscriptionOffer", + "Identifier": "intro-1", + "OfferTags": [ + "tag-a", + "tag-b" + ], + "Phases": [ + { + "$type": "AdaptySubscriptionPhase", + "LocalizedNumberOfPeriods": "1 time", + "LocalizedSubscriptionPeriod": "1 week", + "NumberOfPeriods": 1, + "PaymentMode": "FreeTrial (2)", + "Price": { + "$type": "AdaptyPrice", + "Amount": 0, + "CurrencyCode": "USD", + "CurrencySymbol": "$", + "LocalizedString": "Free" + }, + "SubscriptionPeriod": { + "$type": "AdaptySubscriptionPeriod", + "NumberOfUnits": 1, + "Unit": "Week (1)" + } + } + ], + "Type": "Introductory (0)" + }, + "Period": { + "$type": "AdaptySubscriptionPeriod", + "NumberOfUnits": 1, + "Unit": "Month (2)" + }, + "RenewalType": "Prepaid (0)" + }, + "VendorProductId": "com.adapty.sample.monthly", + "_PayloadData": "{\"custom\":\"product payload\"}", + "_WebPurchaseUrl": "https://pay.adapty.io/checkout/abc" + }, + { + "$type": "AdaptyPaywallProduct", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-2", + "FlowProductId": null, + "IsFamilyShareable": false, + "LocalizedDescription": "One-time purchase", + "LocalizedTitle": "Lifetime", + "PaywallABTestName": "winter_test", + "PaywallName": "Winter Paywall", + "PaywallProductIndex": 1, + "PaywallVariationId": "variation-0001", + "Price": { + "$type": "AdaptyPrice", + "Amount": 99, + "CurrencyCode": "USD", + "CurrencySymbol": "$", + "LocalizedString": "$99.00" + }, + "ProductType": "non_consumable", + "RegionCode": null, + "Subscription": null, + "VendorProductId": "com.adapty.sample.lifetime", + "_PayloadData": null, + "_WebPurchaseUrl": null + } +] diff --git a/tests/shared/Fixtures/approved/products-full.editor.approved.txt b/tests/shared/Fixtures/approved/products-full.editor.approved.txt new file mode 100644 index 0000000..0a73ae1 --- /dev/null +++ b/tests/shared/Fixtures/approved/products-full.editor.approved.txt @@ -0,0 +1,92 @@ +[ + { + "$type": "AdaptyPaywallProduct", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-1", + "FlowProductId": "flow-product-1", + "IsFamilyShareable": false, + "LocalizedDescription": "Full access, billed monthly", + "LocalizedTitle": "Monthly", + "PaywallABTestName": "winter_test", + "PaywallName": "Winter Paywall", + "PaywallProductIndex": 0, + "PaywallVariationId": "variation-0001", + "Price": { + "$type": "AdaptyPrice", + "Amount": 9.99, + "CurrencyCode": "USD", + "CurrencySymbol": "$", + "LocalizedString": "$9.99" + }, + "ProductType": "subscription", + "RegionCode": "US", + "Subscription": { + "$type": "AdaptySubscription", + "BasePlanId": null, + "GroupIdentifier": null, + "LocalizedPeriod": "1 month", + "Offer": { + "$type": "AdaptySubscriptionOffer", + "Identifier": "intro-1", + "OfferTags": null, + "Phases": [ + { + "$type": "AdaptySubscriptionPhase", + "LocalizedNumberOfPeriods": "1 time", + "LocalizedSubscriptionPeriod": "1 week", + "NumberOfPeriods": 1, + "PaymentMode": "FreeTrial (2)", + "Price": { + "$type": "AdaptyPrice", + "Amount": 0, + "CurrencyCode": "USD", + "CurrencySymbol": "$", + "LocalizedString": "Free" + }, + "SubscriptionPeriod": { + "$type": "AdaptySubscriptionPeriod", + "NumberOfUnits": 1, + "Unit": "Week (1)" + } + } + ], + "Type": "Introductory (0)" + }, + "Period": { + "$type": "AdaptySubscriptionPeriod", + "NumberOfUnits": 1, + "Unit": "Month (2)" + }, + "RenewalType": "Autorenewable (1)" + }, + "VendorProductId": "com.adapty.sample.monthly", + "_PayloadData": "{\"custom\":\"product payload\"}", + "_WebPurchaseUrl": "https://pay.adapty.io/checkout/abc" + }, + { + "$type": "AdaptyPaywallProduct", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-2", + "FlowProductId": null, + "IsFamilyShareable": false, + "LocalizedDescription": "One-time purchase", + "LocalizedTitle": "Lifetime", + "PaywallABTestName": "winter_test", + "PaywallName": "Winter Paywall", + "PaywallProductIndex": 1, + "PaywallVariationId": "variation-0001", + "Price": { + "$type": "AdaptyPrice", + "Amount": 99, + "CurrencyCode": "USD", + "CurrencySymbol": "$", + "LocalizedString": "$99.00" + }, + "ProductType": "non_consumable", + "RegionCode": null, + "Subscription": null, + "VendorProductId": "com.adapty.sample.lifetime", + "_PayloadData": null, + "_WebPurchaseUrl": null + } +] diff --git a/tests/shared/Fixtures/approved/products-full.ios.approved.txt b/tests/shared/Fixtures/approved/products-full.ios.approved.txt new file mode 100644 index 0000000..dfe1372 --- /dev/null +++ b/tests/shared/Fixtures/approved/products-full.ios.approved.txt @@ -0,0 +1,92 @@ +[ + { + "$type": "AdaptyPaywallProduct", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-1", + "FlowProductId": "flow-product-1", + "IsFamilyShareable": true, + "LocalizedDescription": "Full access, billed monthly", + "LocalizedTitle": "Monthly", + "PaywallABTestName": "winter_test", + "PaywallName": "Winter Paywall", + "PaywallProductIndex": 0, + "PaywallVariationId": "variation-0001", + "Price": { + "$type": "AdaptyPrice", + "Amount": 9.99, + "CurrencyCode": "USD", + "CurrencySymbol": "$", + "LocalizedString": "$9.99" + }, + "ProductType": "subscription", + "RegionCode": "US", + "Subscription": { + "$type": "AdaptySubscription", + "BasePlanId": null, + "GroupIdentifier": "group-1", + "LocalizedPeriod": "1 month", + "Offer": { + "$type": "AdaptySubscriptionOffer", + "Identifier": "intro-1", + "OfferTags": null, + "Phases": [ + { + "$type": "AdaptySubscriptionPhase", + "LocalizedNumberOfPeriods": "1 time", + "LocalizedSubscriptionPeriod": "1 week", + "NumberOfPeriods": 1, + "PaymentMode": "FreeTrial (2)", + "Price": { + "$type": "AdaptyPrice", + "Amount": 0, + "CurrencyCode": "USD", + "CurrencySymbol": "$", + "LocalizedString": "Free" + }, + "SubscriptionPeriod": { + "$type": "AdaptySubscriptionPeriod", + "NumberOfUnits": 1, + "Unit": "Week (1)" + } + } + ], + "Type": "Introductory (0)" + }, + "Period": { + "$type": "AdaptySubscriptionPeriod", + "NumberOfUnits": 1, + "Unit": "Month (2)" + }, + "RenewalType": "Autorenewable (1)" + }, + "VendorProductId": "com.adapty.sample.monthly", + "_PayloadData": "{\"custom\":\"product payload\"}", + "_WebPurchaseUrl": "https://pay.adapty.io/checkout/abc" + }, + { + "$type": "AdaptyPaywallProduct", + "AccessLevelId": "premium", + "AdaptyProductId": "adapty-product-2", + "FlowProductId": null, + "IsFamilyShareable": false, + "LocalizedDescription": "One-time purchase", + "LocalizedTitle": "Lifetime", + "PaywallABTestName": "winter_test", + "PaywallName": "Winter Paywall", + "PaywallProductIndex": 1, + "PaywallVariationId": "variation-0001", + "Price": { + "$type": "AdaptyPrice", + "Amount": 99, + "CurrencyCode": "USD", + "CurrencySymbol": "$", + "LocalizedString": "$99.00" + }, + "ProductType": "non_consumable", + "RegionCode": null, + "Subscription": null, + "VendorProductId": "com.adapty.sample.lifetime", + "_PayloadData": null, + "_WebPurchaseUrl": null + } +] diff --git a/tests/shared/Fixtures/approved/profile-full.android.approved.txt b/tests/shared/Fixtures/approved/profile-full.android.approved.txt new file mode 100644 index 0000000..b13a98c --- /dev/null +++ b/tests/shared/Fixtures/approved/profile-full.android.approved.txt @@ -0,0 +1,154 @@ +{ + "$type": "AdaptyProfile", + "AccessLevels (property)": { + "premium": { + "$type": "AccessLevel", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "Id": "premium", + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorProductId": "com.adapty.sample.monthly", + "WillRenew": true + } + }, + "AppliedAttributionSources (property)": [ + "appsflyer", + "adjust" + ], + "CustomAttributes (property)": { + "favourite_colour": "green", + "score": 12.5 + }, + "CustomerUserId": "user-42", + "IsTestUser": true, + "NonSubscriptions (property)": { + "com.adapty.sample.coins": [ + { + "$type": "NonSubscription", + "IsConsumable": true, + "IsRefund": false, + "IsSandbox": true, + "PurchaseId": "a1b2c3d4-0000-4444-8888-999900001111", + "PurchasedAt": { "utc": "2026-06-01T12:00:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "VendorProductId": "com.adapty.sample.coins", + "VendorTransactionId": "2000000987654321" + } + ] + }, + "ProfileId": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "SegmentId": "8f14e45fceea167a", + "Subscriptions (property)": { + "com.adapty.sample.monthly": { + "$type": "Subscription", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "IsSandbox": true, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorOriginalTransactionId": "2000000000000001", + "VendorProductId": "com.adapty.sample.monthly", + "VendorTransactionId": "2000000123456789", + "WillRenew": true + } + }, + "Version": 1753876800000, + "_AccessLevels": { + "premium": { + "$type": "AccessLevel", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "Id": "premium", + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorProductId": "com.adapty.sample.monthly", + "WillRenew": true + } + }, + "_AppliedAttributionSources": [ + "appsflyer", + "adjust" + ], + "_CustomAttributes": { + "favourite_colour": "green", + "score": 12.5 + }, + "_NonSubscriptions": { + "com.adapty.sample.coins": [ + { + "$type": "NonSubscription", + "IsConsumable": true, + "IsRefund": false, + "IsSandbox": true, + "PurchaseId": "a1b2c3d4-0000-4444-8888-999900001111", + "PurchasedAt": { "utc": "2026-06-01T12:00:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "VendorProductId": "com.adapty.sample.coins", + "VendorTransactionId": "2000000987654321" + } + ] + }, + "_Subscriptions": { + "com.adapty.sample.monthly": { + "$type": "Subscription", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "IsSandbox": true, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorOriginalTransactionId": "2000000000000001", + "VendorProductId": "com.adapty.sample.monthly", + "VendorTransactionId": "2000000123456789", + "WillRenew": true + } + } +} diff --git a/tests/shared/Fixtures/approved/profile-full.editor.approved.txt b/tests/shared/Fixtures/approved/profile-full.editor.approved.txt new file mode 100644 index 0000000..b13a98c --- /dev/null +++ b/tests/shared/Fixtures/approved/profile-full.editor.approved.txt @@ -0,0 +1,154 @@ +{ + "$type": "AdaptyProfile", + "AccessLevels (property)": { + "premium": { + "$type": "AccessLevel", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "Id": "premium", + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorProductId": "com.adapty.sample.monthly", + "WillRenew": true + } + }, + "AppliedAttributionSources (property)": [ + "appsflyer", + "adjust" + ], + "CustomAttributes (property)": { + "favourite_colour": "green", + "score": 12.5 + }, + "CustomerUserId": "user-42", + "IsTestUser": true, + "NonSubscriptions (property)": { + "com.adapty.sample.coins": [ + { + "$type": "NonSubscription", + "IsConsumable": true, + "IsRefund": false, + "IsSandbox": true, + "PurchaseId": "a1b2c3d4-0000-4444-8888-999900001111", + "PurchasedAt": { "utc": "2026-06-01T12:00:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "VendorProductId": "com.adapty.sample.coins", + "VendorTransactionId": "2000000987654321" + } + ] + }, + "ProfileId": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "SegmentId": "8f14e45fceea167a", + "Subscriptions (property)": { + "com.adapty.sample.monthly": { + "$type": "Subscription", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "IsSandbox": true, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorOriginalTransactionId": "2000000000000001", + "VendorProductId": "com.adapty.sample.monthly", + "VendorTransactionId": "2000000123456789", + "WillRenew": true + } + }, + "Version": 1753876800000, + "_AccessLevels": { + "premium": { + "$type": "AccessLevel", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "Id": "premium", + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorProductId": "com.adapty.sample.monthly", + "WillRenew": true + } + }, + "_AppliedAttributionSources": [ + "appsflyer", + "adjust" + ], + "_CustomAttributes": { + "favourite_colour": "green", + "score": 12.5 + }, + "_NonSubscriptions": { + "com.adapty.sample.coins": [ + { + "$type": "NonSubscription", + "IsConsumable": true, + "IsRefund": false, + "IsSandbox": true, + "PurchaseId": "a1b2c3d4-0000-4444-8888-999900001111", + "PurchasedAt": { "utc": "2026-06-01T12:00:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "VendorProductId": "com.adapty.sample.coins", + "VendorTransactionId": "2000000987654321" + } + ] + }, + "_Subscriptions": { + "com.adapty.sample.monthly": { + "$type": "Subscription", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "IsSandbox": true, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorOriginalTransactionId": "2000000000000001", + "VendorProductId": "com.adapty.sample.monthly", + "VendorTransactionId": "2000000123456789", + "WillRenew": true + } + } +} diff --git a/tests/shared/Fixtures/approved/profile-full.ios.approved.txt b/tests/shared/Fixtures/approved/profile-full.ios.approved.txt new file mode 100644 index 0000000..b13a98c --- /dev/null +++ b/tests/shared/Fixtures/approved/profile-full.ios.approved.txt @@ -0,0 +1,154 @@ +{ + "$type": "AdaptyProfile", + "AccessLevels (property)": { + "premium": { + "$type": "AccessLevel", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "Id": "premium", + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorProductId": "com.adapty.sample.monthly", + "WillRenew": true + } + }, + "AppliedAttributionSources (property)": [ + "appsflyer", + "adjust" + ], + "CustomAttributes (property)": { + "favourite_colour": "green", + "score": 12.5 + }, + "CustomerUserId": "user-42", + "IsTestUser": true, + "NonSubscriptions (property)": { + "com.adapty.sample.coins": [ + { + "$type": "NonSubscription", + "IsConsumable": true, + "IsRefund": false, + "IsSandbox": true, + "PurchaseId": "a1b2c3d4-0000-4444-8888-999900001111", + "PurchasedAt": { "utc": "2026-06-01T12:00:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "VendorProductId": "com.adapty.sample.coins", + "VendorTransactionId": "2000000987654321" + } + ] + }, + "ProfileId": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "SegmentId": "8f14e45fceea167a", + "Subscriptions (property)": { + "com.adapty.sample.monthly": { + "$type": "Subscription", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "IsSandbox": true, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorOriginalTransactionId": "2000000000000001", + "VendorProductId": "com.adapty.sample.monthly", + "VendorTransactionId": "2000000123456789", + "WillRenew": true + } + }, + "Version": 1753876800000, + "_AccessLevels": { + "premium": { + "$type": "AccessLevel", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "Id": "premium", + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorProductId": "com.adapty.sample.monthly", + "WillRenew": true + } + }, + "_AppliedAttributionSources": [ + "appsflyer", + "adjust" + ], + "_CustomAttributes": { + "favourite_colour": "green", + "score": 12.5 + }, + "_NonSubscriptions": { + "com.adapty.sample.coins": [ + { + "$type": "NonSubscription", + "IsConsumable": true, + "IsRefund": false, + "IsSandbox": true, + "PurchaseId": "a1b2c3d4-0000-4444-8888-999900001111", + "PurchasedAt": { "utc": "2026-06-01T12:00:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "VendorProductId": "com.adapty.sample.coins", + "VendorTransactionId": "2000000987654321" + } + ] + }, + "_Subscriptions": { + "com.adapty.sample.monthly": { + "$type": "Subscription", + "ActivatedAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "ActiveIntroductoryOfferType": "free_trial", + "ActivePromotionalOfferId": "promo-winter", + "ActivePromotionalOfferType": "promotional", + "BillingIssueDetectedAt": { "utc": "2026-07-19T10:00:00.0000000Z", "kind": "Local" }, + "CancellationReason": "voluntarily_cancelled", + "ExpiresAt": { "utc": "2026-08-15T09:30:00.0000000Z", "kind": "Local" }, + "IsActive": true, + "IsInGracePeriod": false, + "IsLifetime": false, + "IsRefund": false, + "IsSandbox": true, + "OfferId": "offer-1", + "RenewedAt": { "utc": "2026-07-15T09:30:00.0000000Z", "kind": "Local" }, + "StartsAt": { "utc": "2026-01-15T09:30:00.0000000Z", "kind": "Local" }, + "Store": "app_store", + "UnsubscribedAt": { "utc": "2026-07-20T10:00:00.0000000Z", "kind": "Local" }, + "VendorOriginalTransactionId": "2000000000000001", + "VendorProductId": "com.adapty.sample.monthly", + "VendorTransactionId": "2000000123456789", + "WillRenew": true + } + } +} diff --git a/tests/shared/Fixtures/approved/profile-minimal.android.approved.txt b/tests/shared/Fixtures/approved/profile-minimal.android.approved.txt new file mode 100644 index 0000000..baf32c7 --- /dev/null +++ b/tests/shared/Fixtures/approved/profile-minimal.android.approved.txt @@ -0,0 +1,18 @@ +{ + "$type": "AdaptyProfile", + "AccessLevels (property)": {}, + "AppliedAttributionSources (property)": [], + "CustomAttributes (property)": {}, + "CustomerUserId": null, + "IsTestUser": false, + "NonSubscriptions (property)": {}, + "ProfileId": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "SegmentId": "8f14e45fceea167a", + "Subscriptions (property)": {}, + "Version": 1753876800000, + "_AccessLevels": {}, + "_AppliedAttributionSources": [], + "_CustomAttributes": {}, + "_NonSubscriptions": {}, + "_Subscriptions": {} +} diff --git a/tests/shared/Fixtures/approved/profile-minimal.editor.approved.txt b/tests/shared/Fixtures/approved/profile-minimal.editor.approved.txt new file mode 100644 index 0000000..baf32c7 --- /dev/null +++ b/tests/shared/Fixtures/approved/profile-minimal.editor.approved.txt @@ -0,0 +1,18 @@ +{ + "$type": "AdaptyProfile", + "AccessLevels (property)": {}, + "AppliedAttributionSources (property)": [], + "CustomAttributes (property)": {}, + "CustomerUserId": null, + "IsTestUser": false, + "NonSubscriptions (property)": {}, + "ProfileId": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "SegmentId": "8f14e45fceea167a", + "Subscriptions (property)": {}, + "Version": 1753876800000, + "_AccessLevels": {}, + "_AppliedAttributionSources": [], + "_CustomAttributes": {}, + "_NonSubscriptions": {}, + "_Subscriptions": {} +} diff --git a/tests/shared/Fixtures/approved/profile-minimal.ios.approved.txt b/tests/shared/Fixtures/approved/profile-minimal.ios.approved.txt new file mode 100644 index 0000000..baf32c7 --- /dev/null +++ b/tests/shared/Fixtures/approved/profile-minimal.ios.approved.txt @@ -0,0 +1,18 @@ +{ + "$type": "AdaptyProfile", + "AccessLevels (property)": {}, + "AppliedAttributionSources (property)": [], + "CustomAttributes (property)": {}, + "CustomerUserId": null, + "IsTestUser": false, + "NonSubscriptions (property)": {}, + "ProfileId": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "SegmentId": "8f14e45fceea167a", + "Subscriptions (property)": {}, + "Version": 1753876800000, + "_AccessLevels": {}, + "_AppliedAttributionSources": [], + "_CustomAttributes": {}, + "_NonSubscriptions": {}, + "_Subscriptions": {} +} diff --git a/tests/shared/Fixtures/approved/public-surface.android.approved.txt b/tests/shared/Fixtures/approved/public-surface.android.approved.txt new file mode 100644 index 0000000..d2e1cb7 --- /dev/null +++ b/tests/shared/Fixtures/approved/public-surface.android.approved.txt @@ -0,0 +1,653 @@ +protected AdaptySDK.AdaptyCustomAsset.ctor() +protected AdaptySDK.AdaptyOnboardingsAnalyticsEvent.ctor() +protected AdaptySDK.AdaptyOnboardingsInput.ctor() +protected AdaptySDK.AdaptyOnboardingsStateUpdatedParams.ctor() +public AdaptySDK.AdaptyConfiguration+Builder.ActivateUI : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.AdaptyUIMediaCache : AdaptySDK.AdaptyUIMediaCacheConfiguration +public AdaptySDK.AdaptyConfiguration+Builder.ApiKey : System.String +public AdaptySDK.AdaptyConfiguration+Builder.AppleClearDataOnBackup : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.AppleIdfaCollectionDisabled : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.BackendProxyHost : System.String +public AdaptySDK.AdaptyConfiguration+Builder.BackendProxyPort : System.Int32 +public AdaptySDK.AdaptyConfiguration+Builder.Build() : AdaptySDK.AdaptyConfiguration +public AdaptySDK.AdaptyConfiguration+Builder.CustomerIdentity : AdaptySDK.AdaptyCustomerIdentity +public AdaptySDK.AdaptyConfiguration+Builder.CustomerUserId : System.String +public AdaptySDK.AdaptyConfiguration+Builder.GoogleAdvertisingIdCollectionDisabled : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.GoogleEnablePendingPrepaidPlans : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.GoogleLocalAccessLevelAllowed : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.IpAddressCollectionDisabled : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.LogLevel : AdaptySDK.AdaptyLogLevel +public AdaptySDK.AdaptyConfiguration+Builder.ObserverMode : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.ServerCluster : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.SetAPIKey(System.String apiKey) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetActivateUI(System.Boolean activate) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetAdaptyUIMediaCache(System.Nullable memoryStorageTotalCostLimit, System.Nullable memoryStorageCountLimit, System.Nullable diskStorageSizeLimit) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetAppleClearDataOnBackup(System.Boolean appleClearDataOnBackup) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetAppleIDFACollectionDisabled(System.Boolean appleIdfaCollectionDisabled) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetBackendProxy(System.String host, System.Int32 port) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetCustomerUserId(System.String customerUserId) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetCustomerUserId(System.String customerUserId, System.Guid iosAppAccountToken, System.String androidObfuscatedAccountId) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetGoogleAdvertisingIdCollectionDisabled(System.Boolean googleAdvertisingIdCollectionDisabled) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetGoogleEnablePendingPrepaidPlans(System.Boolean googleEnablePendingPrepaidPlans) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetGoogleLocalAccessLevelAllowed(System.Boolean googleLocalAccessLevelAllowed) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetIPAddressCollectionDisabled(System.Boolean ipAddressCollectionDisabled) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetObserverMode(System.Boolean observerMode) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetServerCluster(AdaptySDK.AdaptyServerCluster serverCluster) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.ctor(System.String apiKey) +public AdaptySDK.AdaptyCustomAssetColor.ColorValue : UnityEngine.Color { get; } +public AdaptySDK.AdaptyCustomAssetLinearGradient.Gradient : UnityEngine.Gradient { get; } +public AdaptySDK.AdaptyCustomAssetLocalImageAsset.AssetId : System.String { get; } +public AdaptySDK.AdaptyCustomAssetLocalImageData.Data : System.Byte[] { get; } +public AdaptySDK.AdaptyCustomAssetLocalImageFile.Path : System.String { get; } +public AdaptySDK.AdaptyCustomAssetLocalVideoAsset.AssetId : System.String { get; } +public AdaptySDK.AdaptyCustomAssetLocalVideoFile.Path : System.String { get; } +public AdaptySDK.AdaptyCustomerIdentity.IsEmpty : System.Boolean { get; } +public AdaptySDK.AdaptyCustomerIdentity.ctor(System.Guid iosAppAccountToken, System.String androidObfuscatedAccountId) +public AdaptySDK.AdaptyErrorCode.value__ : System.Int32 +public AdaptySDK.AdaptyFlow.Paywalls : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlow.ProductIdentifiers : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlow.RemoteConfig : AdaptySDK.AdaptyRemoteConfig { get; } +public AdaptySDK.AdaptyFlow.RemoteConfigs : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlow.VendorProductIds : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlowPaywall.ProductIdentifiers : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlowPaywall.VendorProductIds : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyInstallationStatus.Details : AdaptySDK.AdaptyInstallationDetails { get; } +public AdaptySDK.AdaptyInstallationStatusType.value__ : System.Int32 +public AdaptySDK.AdaptyLogLevel.value__ : System.Int32 +public AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingCompleted.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingStarted.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventProductsScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventRegistrationScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted.ctor(System.String elementId, System.String reply) +public AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventSecondScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventUnknown.ctor(System.String name) +public AdaptySDK.AdaptyOnboardingsAnalyticsEventUserEmailCollected.ctor() +public AdaptySDK.AdaptyOnboardingsDatePickerParams.ctor(System.Nullable day, System.Nullable month, System.Nullable year) +public AdaptySDK.AdaptyOnboardingsEmailInput.ctor(System.String value) +public AdaptySDK.AdaptyOnboardingsInputParams.ctor(AdaptySDK.AdaptyOnboardingsInput input) +public AdaptySDK.AdaptyOnboardingsMultiSelectParams.ctor(System.Collections.Generic.IList params) +public AdaptySDK.AdaptyOnboardingsNumberInput.ctor(System.Double value) +public AdaptySDK.AdaptyOnboardingsSelectParams.ctor(System.String id, System.String value, System.String label) +public AdaptySDK.AdaptyOnboardingsTextInput.ctor(System.String value) +public AdaptySDK.AdaptyPaymentMode.value__ : System.Int32 +public AdaptySDK.AdaptyProductIdentifier.ctor(System.String vendorProductId, System.String adaptyProductId, System.String basePlanId) +public AdaptySDK.AdaptyProfile.AccessLevels : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfile.AppliedAttributionSources : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyProfile.CustomAttributes : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfile.NonSubscriptions : System.Collections.Generic.IReadOnlyDictionary> { get; } +public AdaptySDK.AdaptyProfile.Subscriptions : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfileGender.value__ : System.Int32 +public AdaptySDK.AdaptyProfileParameters+Builder.Build() : AdaptySDK.AdaptyProfileParameters +public AdaptySDK.AdaptyProfileParameters+Builder.RemoveCustomAttribute(System.String key) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetAnalyticsDisabled(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetAppTrackingTransparencyStatus(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetBirthday(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetCustomDoubleAttribute(System.String key, System.Double value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetCustomStringAttribute(System.String key, System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetEmail(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetFirstName(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetGender(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetLastName(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetPhoneNumber(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.ctor() +public AdaptySDK.AdaptyProfileParameters.AnalyticsDisabled : System.Nullable +public AdaptySDK.AdaptyProfileParameters.AppTrackingTransparencyStatus : System.Nullable +public AdaptySDK.AdaptyProfileParameters.Birthday : System.Nullable +public AdaptySDK.AdaptyProfileParameters.CustomAttributes : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfileParameters.Email : System.String +public AdaptySDK.AdaptyProfileParameters.FirstName : System.String +public AdaptySDK.AdaptyProfileParameters.Gender : System.Nullable +public AdaptySDK.AdaptyProfileParameters.LastName : System.String +public AdaptySDK.AdaptyProfileParameters.PhoneNumber : System.String +public AdaptySDK.AdaptyProfileParameters.RemoveCustomAttribute(System.String key) : System.Void +public AdaptySDK.AdaptyProfileParameters.SetCustomDoubleAttribute(System.String key, System.Double value) : System.Void +public AdaptySDK.AdaptyProfileParameters.SetCustomStringAttribute(System.String key, System.String value) : System.Void +public AdaptySDK.AdaptyProfileParameters.ctor() +public AdaptySDK.AdaptyPurchaseParameters.ctor(AdaptySDK.AdaptySubscriptionUpdateParameters subscriptionUpdateParams = null, System.Nullable isOfferPersonalized = null) +public AdaptySDK.AdaptyPurchaseParametersBuilder.Build() : AdaptySDK.AdaptyPurchaseParameters +public AdaptySDK.AdaptyPurchaseParametersBuilder.SetIsOfferPersonalized(System.Nullable isOfferPersonalized) : AdaptySDK.AdaptyPurchaseParametersBuilder +public AdaptySDK.AdaptyPurchaseParametersBuilder.SetSubscriptionUpdateParams(AdaptySDK.AdaptySubscriptionUpdateParameters subscriptionUpdateParams) : AdaptySDK.AdaptyPurchaseParametersBuilder +public AdaptySDK.AdaptyPurchaseParametersBuilder.ctor() +public AdaptySDK.AdaptyPurchaseResultType.value__ : System.Int32 +public AdaptySDK.AdaptyRefundPreference.value__ : System.Int32 +public AdaptySDK.AdaptyRemoteConfig.Dictionary : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyServerCluster.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionOfferType.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionPeriodUnit.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionRenewalType.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionUpdateParameters.OldSubVendorProductId : System.String +public AdaptySDK.AdaptySubscriptionUpdateParameters.ReplacementMode : AdaptySDK.AdaptySubscriptionUpdateReplacementMode +public AdaptySDK.AdaptySubscriptionUpdateParameters.ctor(System.String oldSubVendorProductId, AdaptySDK.AdaptySubscriptionUpdateReplacementMode replacementMode) +public AdaptySDK.AdaptySubscriptionUpdateReplacementMode.value__ : System.Int32 +public AdaptySDK.AdaptyUICreateFlowViewParameters.CustomAssets : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.CustomTags : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.CustomTimers : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.EnableSafeAreaPaddings : System.Nullable +public AdaptySDK.AdaptyUICreateFlowViewParameters.LoadTimeout : System.Nullable +public AdaptySDK.AdaptyUICreateFlowViewParameters.Locale : System.String +public AdaptySDK.AdaptyUICreateFlowViewParameters.PreloadProducts : System.Nullable +public AdaptySDK.AdaptyUICreateFlowViewParameters.ProductPurchaseParameters : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetCustomAssets(System.Collections.Generic.IReadOnlyDictionary customAssets) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetCustomTags(System.Collections.Generic.IReadOnlyDictionary customTags) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetCustomTimers(System.Collections.Generic.IReadOnlyDictionary customTimers) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetEnableSafeAreaPaddings(System.Nullable enableSafeAreaPaddings) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetLoadTimeout(System.Nullable loadTimeout) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetLocale(System.String locale) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetPreloadProducts(System.Nullable preloadProducts) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetProductPurchaseParameters(System.Collections.Generic.IReadOnlyDictionary productPurchaseParameters) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.ctor() +public AdaptySDK.AdaptyUIDialogActionType.value__ : System.Int32 +public AdaptySDK.AdaptyUIDialogConfiguration.Content : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.DefaultActionTitle : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.SecondaryActionTitle : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.SetContent(System.String content) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.SetDefaultActionTitle(System.String defaultActionTitle) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.SetSecondaryActionTitle(System.String secondaryActionTitle) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.SetTitle(System.String title) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.Title : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.ctor() +public AdaptySDK.AdaptyUIFlowView.Dismiss(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIFlowView.Id : System.String +public AdaptySDK.AdaptyUIFlowView.Locale : System.String +public AdaptySDK.AdaptyUIFlowView.PlacementId : System.String +public AdaptySDK.AdaptyUIFlowView.Present(AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIFlowView.Present(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIFlowView.VariationId : System.String +public AdaptySDK.AdaptyUIIOSPresentationStyle.value__ : System.Int32 +public AdaptySDK.AdaptyUIMediaCacheConfiguration.DiskStorageSizeLimit : System.Nullable +public AdaptySDK.AdaptyUIMediaCacheConfiguration.MemoryStorageCountLimit : System.Nullable +public AdaptySDK.AdaptyUIMediaCacheConfiguration.MemoryStorageTotalCostLimit : System.Nullable +public AdaptySDK.AdaptyUIMediaCacheConfiguration.ctor(System.Nullable memoryStorageTotalCostLimit, System.Nullable memoryStorageCountLimit, System.Nullable diskStorageSizeLimit) +public AdaptySDK.AdaptyUIOnboardingView.Dismiss(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIOnboardingView.Id : System.String +public AdaptySDK.AdaptyUIOnboardingView.PaywallVariationId : System.String +public AdaptySDK.AdaptyUIOnboardingView.PlacementId : System.String +public AdaptySDK.AdaptyUIOnboardingView.Present(AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIOnboardingView.Present(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIUserAction.OpenIn : System.Nullable +public AdaptySDK.AdaptyUIUserAction.Type : AdaptySDK.AdaptyUIUserActionType +public AdaptySDK.AdaptyUIUserAction.Value : System.String +public AdaptySDK.AdaptyUIUserActionType.value__ : System.Int32 +public AdaptySDK.AdaptyWebPresentation.value__ : System.Int32 +public AdaptySDK.AppTrackingTransparencyStatus.value__ : System.Int32 +public abstract AdaptySDK.IAdaptyEventListener.OnInstallationDetailsFail(AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyEventListener.OnInstallationDetailsSuccess(AdaptySDK.AdaptyInstallationDetails details) : System.Void +public abstract AdaptySDK.IAdaptyEventListener.OnLoadLatestProfile(AdaptySDK.AdaptyProfile profile) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidAppear(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidDisappear(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFailLoadingProducts(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFailPurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFailRestore(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFinishPurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyPurchaseResult purchasedResult) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFinishRestore(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyProfile profile) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFinishWebPaymentNavigation(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidPerformAction(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyUIUserAction action) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidReceiveAnalyticEvent(AdaptySDK.AdaptyUIFlowView view, System.String name, System.Collections.Generic.IReadOnlyDictionary parameters) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidReceiveError(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidSelectProduct(AdaptySDK.AdaptyUIFlowView view, System.String productId) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidStartPurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidStartRestore(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewDidFailWithError(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewDidFinishLoading(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnAnalyticsEvent(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, AdaptySDK.AdaptyOnboardingsAnalyticsEvent analyticsEvent) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnCloseAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String actionId) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnCustomAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String actionId) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnPaywallAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String actionId) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnStateUpdatedAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String elementId, AdaptySDK.AdaptyOnboardingsStateUpdatedParams params) : System.Void +public abstract AdaptySDK.IAdaptyUIObserverModeResolver.FlowViewDidInitiatePurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, System.Action onStartPurchase, System.Action onFinishPurchase) : System.Void +public abstract AdaptySDK.IAdaptyUIObserverModeResolver.FlowViewDidInitiateRestore(AdaptySDK.AdaptyUIFlowView view, System.Action onStartRestore, System.Action onFinishRestore) : System.Void +public abstract AdaptySDK.IAdaptyUISystemRequestsHandler.FlowViewDidAskPermission(AdaptySDK.AdaptyUIFlowView view, System.String permission, System.Collections.Generic.IReadOnlyDictionary customArgs, System.Action respond) : System.Void +public abstract AdaptySDK.IAdaptyUISystemRequestsHandler.FlowViewDidRequestAppReview(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract class AdaptySDK.AdaptyCustomAsset +public abstract class AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public abstract class AdaptySDK.AdaptyOnboardingsInput +public abstract class AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public enum AdaptySDK.AdaptyErrorCode : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyInstallationStatusType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyLogLevel : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyPaymentMode : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyProfileGender : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyPurchaseResultType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyRefundPreference : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyServerCluster : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionOfferType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionPeriodUnit : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionRenewalType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionUpdateReplacementMode : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyUIDialogActionType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyUIIOSPresentationStyle : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyUIUserActionType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyWebPresentation : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AppTrackingTransparencyStatus : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public interface AdaptySDK.IAdaptyEventListener +public interface AdaptySDK.IAdaptyFlowsEventsListener +public interface AdaptySDK.IAdaptyOnboardingsEventsListener +public interface AdaptySDK.IAdaptyUIObserverModeResolver +public interface AdaptySDK.IAdaptyUISystemRequestsHandler +public override AdaptySDK.AdaptyConfiguration+Builder.ToString() : System.String +public override AdaptySDK.AdaptyConfiguration.ToString() : System.String +public override AdaptySDK.AdaptyCustomerIdentity.ToString() : System.String +public override AdaptySDK.AdaptyError.ToString() : System.String +public override AdaptySDK.AdaptyFlow.ToString() : System.String +public override AdaptySDK.AdaptyFlowPaywall.ToString() : System.String +public override AdaptySDK.AdaptyInstallationDetails.ToString() : System.String +public override AdaptySDK.AdaptyInstallationStatus.ToString() : System.String +public override AdaptySDK.AdaptyOnboarding.ToString() : System.String +public override AdaptySDK.AdaptyOnboardingsDatePickerParams.ToString() : System.String +public override AdaptySDK.AdaptyOnboardingsMultiSelectParams.ToString() : System.String +public override AdaptySDK.AdaptyOnboardingsSelectParams.ToString() : System.String +public override AdaptySDK.AdaptyPaywallProduct.ToString() : System.String +public override AdaptySDK.AdaptyPlacement.ToString() : System.String +public override AdaptySDK.AdaptyPlacementFetchPolicy.ToString() : System.String +public override AdaptySDK.AdaptyPrice.ToString() : System.String +public override AdaptySDK.AdaptyProductIdentifier.Equals(System.Object obj) : System.Boolean +public override AdaptySDK.AdaptyProductIdentifier.GetHashCode() : System.Int32 +public override AdaptySDK.AdaptyProductIdentifier.ToString() : System.String +public override AdaptySDK.AdaptyProfile+AccessLevel.ToString() : System.String +public override AdaptySDK.AdaptyProfile+NonSubscription.ToString() : System.String +public override AdaptySDK.AdaptyProfile+Subscription.ToString() : System.String +public override AdaptySDK.AdaptyProfile.ToString() : System.String +public override AdaptySDK.AdaptyProfileParameters.ToString() : System.String +public override AdaptySDK.AdaptyPurchaseParameters.ToString() : System.String +public override AdaptySDK.AdaptyPurchaseResult.ToString() : System.String +public override AdaptySDK.AdaptySubscription.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionOffer.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionPeriod.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionPhase.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionUpdateParameters.ToString() : System.String +public override AdaptySDK.AdaptyUICreateFlowViewParameters.ToString() : System.String +public override AdaptySDK.AdaptyUIDialogConfiguration.ToString() : System.String +public override AdaptySDK.AdaptyUIFlowView.ToString() : System.String +public override AdaptySDK.AdaptyUIMediaCacheConfiguration.ToString() : System.String +public override AdaptySDK.AdaptyUIOnboardingMeta.ToString() : System.String +public override AdaptySDK.AdaptyUIOnboardingView.ToString() : System.String +public override AdaptySDK.AdaptyUIUserAction.ToString() : System.String +public readonly AdaptySDK.AdaptyCustomerIdentity.AndroidObfuscatedAccountId : System.String +public readonly AdaptySDK.AdaptyCustomerIdentity.IosAppAccountToken : System.Guid +public readonly AdaptySDK.AdaptyError.Code : AdaptySDK.AdaptyErrorCode +public readonly AdaptySDK.AdaptyError.Detail : System.String +public readonly AdaptySDK.AdaptyError.Message : System.String +public readonly AdaptySDK.AdaptyFlow.FlowVersionId : System.String +public readonly AdaptySDK.AdaptyFlow.InstanceIdentity : System.String +public readonly AdaptySDK.AdaptyFlow.Name : System.String +public readonly AdaptySDK.AdaptyFlow.Placement : AdaptySDK.AdaptyPlacement +public readonly AdaptySDK.AdaptyFlow.VariationId : System.String +public readonly AdaptySDK.AdaptyFlowPaywall.InstanceIdentity : System.String +public readonly AdaptySDK.AdaptyFlowPaywall.Name : System.String +public readonly AdaptySDK.AdaptyFlowPaywall.Placement : AdaptySDK.AdaptyPlacement +public readonly AdaptySDK.AdaptyFlowPaywall.VariationId : System.String +public readonly AdaptySDK.AdaptyInstallationDetails.AppLaunchCount : System.Int32 +public readonly AdaptySDK.AdaptyInstallationDetails.InstallId : System.String +public readonly AdaptySDK.AdaptyInstallationDetails.InstallTime : System.DateTime +public readonly AdaptySDK.AdaptyInstallationDetails.Payload : System.String +public readonly AdaptySDK.AdaptyInstallationStatus.Status : AdaptySDK.AdaptyInstallationStatusType +public readonly AdaptySDK.AdaptyOnboarding.Name : System.String +public readonly AdaptySDK.AdaptyOnboarding.OnboardingId : System.String +public readonly AdaptySDK.AdaptyOnboarding.Placement : AdaptySDK.AdaptyPlacement +public readonly AdaptySDK.AdaptyOnboarding.RemoteConfig : AdaptySDK.AdaptyRemoteConfig +public readonly AdaptySDK.AdaptyOnboarding.VariationId : System.String +public readonly AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted.ElementId : System.String +public readonly AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted.Reply : System.String +public readonly AdaptySDK.AdaptyOnboardingsAnalyticsEventUnknown.Name : System.String +public readonly AdaptySDK.AdaptyOnboardingsDatePickerParams.Day : System.Nullable +public readonly AdaptySDK.AdaptyOnboardingsDatePickerParams.Month : System.Nullable +public readonly AdaptySDK.AdaptyOnboardingsDatePickerParams.Year : System.Nullable +public readonly AdaptySDK.AdaptyOnboardingsEmailInput.Value : System.String +public readonly AdaptySDK.AdaptyOnboardingsInputParams.Input : AdaptySDK.AdaptyOnboardingsInput +public readonly AdaptySDK.AdaptyOnboardingsMultiSelectParams.Params : System.Collections.Generic.IList +public readonly AdaptySDK.AdaptyOnboardingsNumberInput.Value : System.Double +public readonly AdaptySDK.AdaptyOnboardingsSelectParams.Id : System.String +public readonly AdaptySDK.AdaptyOnboardingsSelectParams.Label : System.String +public readonly AdaptySDK.AdaptyOnboardingsSelectParams.Value : System.String +public readonly AdaptySDK.AdaptyOnboardingsTextInput.Value : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.AccessLevelId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.AdaptyProductId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.FlowProductId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.IsFamilyShareable : System.Boolean +public readonly AdaptySDK.AdaptyPaywallProduct.LocalizedDescription : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.LocalizedTitle : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallABTestName : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallName : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallProductIndex : System.Int32 +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallVariationId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.Price : AdaptySDK.AdaptyPrice +public readonly AdaptySDK.AdaptyPaywallProduct.ProductType : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.RegionCode : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.Subscription : AdaptySDK.AdaptySubscription +public readonly AdaptySDK.AdaptyPaywallProduct.VendorProductId : System.String +public readonly AdaptySDK.AdaptyPlacement.ABTestName : System.String +public readonly AdaptySDK.AdaptyPlacement.AudienceName : System.String +public readonly AdaptySDK.AdaptyPlacement.Id : System.String +public readonly AdaptySDK.AdaptyPlacement.IsTrackingPurchases : System.Nullable +public readonly AdaptySDK.AdaptyPlacement.PlacementAudienceVersionId : System.String +public readonly AdaptySDK.AdaptyPlacement.Revision : System.Int64 +public readonly AdaptySDK.AdaptyPrice.Amount : System.Double +public readonly AdaptySDK.AdaptyPrice.CurrencyCode : System.String +public readonly AdaptySDK.AdaptyPrice.CurrencySymbol : System.String +public readonly AdaptySDK.AdaptyPrice.LocalizedString : System.String +public readonly AdaptySDK.AdaptyProductIdentifier.BasePlanId : System.String +public readonly AdaptySDK.AdaptyProductIdentifier.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActivatedAt : System.DateTime +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActiveIntroductoryOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActivePromotionalOfferId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActivePromotionalOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.BillingIssueDetectedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.CancellationReason : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ExpiresAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.Id : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsActive : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsInGracePeriod : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsLifetime : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsRefund : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.OfferId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.RenewedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.StartsAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.Store : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.UnsubscribedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.WillRenew : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.IsConsumable : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.IsRefund : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.IsSandbox : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.PurchaseId : System.String +public readonly AdaptySDK.AdaptyProfile+NonSubscription.PurchasedAt : System.DateTime +public readonly AdaptySDK.AdaptyProfile+NonSubscription.Store : System.String +public readonly AdaptySDK.AdaptyProfile+NonSubscription.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+NonSubscription.VendorTransactionId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ActivatedAt : System.DateTime +public readonly AdaptySDK.AdaptyProfile+Subscription.ActiveIntroductoryOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ActivePromotionalOfferId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ActivePromotionalOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.BillingIssueDetectedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.CancellationReason : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ExpiresAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.IsActive : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsInGracePeriod : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsLifetime : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsRefund : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsSandbox : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.OfferId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.RenewedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.StartsAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.Store : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.UnsubscribedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.VendorOriginalTransactionId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.VendorTransactionId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.WillRenew : System.Boolean +public readonly AdaptySDK.AdaptyProfile.CustomerUserId : System.String +public readonly AdaptySDK.AdaptyProfile.ProfileId : System.String +public readonly AdaptySDK.AdaptyPurchaseParameters.IsOfferPersonalized : System.Nullable +public readonly AdaptySDK.AdaptyPurchaseParameters.SubscriptionUpdateParams : AdaptySDK.AdaptySubscriptionUpdateParameters +public readonly AdaptySDK.AdaptyPurchaseResult.AppleJWSTransaction : System.String +public readonly AdaptySDK.AdaptyPurchaseResult.GooglePurchaseToken : System.String +public readonly AdaptySDK.AdaptyPurchaseResult.Profile : AdaptySDK.AdaptyProfile +public readonly AdaptySDK.AdaptyPurchaseResult.Type : AdaptySDK.AdaptyPurchaseResultType +public readonly AdaptySDK.AdaptyRemoteConfig.Data : System.String +public readonly AdaptySDK.AdaptyRemoteConfig.Locale : System.String +public readonly AdaptySDK.AdaptySubscription.BasePlanId : System.String +public readonly AdaptySDK.AdaptySubscription.GroupIdentifier : System.String +public readonly AdaptySDK.AdaptySubscription.LocalizedPeriod : System.String +public readonly AdaptySDK.AdaptySubscription.Offer : AdaptySDK.AdaptySubscriptionOffer +public readonly AdaptySDK.AdaptySubscription.Period : AdaptySDK.AdaptySubscriptionPeriod +public readonly AdaptySDK.AdaptySubscription.RenewalType : AdaptySDK.AdaptySubscriptionRenewalType +public readonly AdaptySDK.AdaptySubscriptionOffer.Identifier : System.String +public readonly AdaptySDK.AdaptySubscriptionOffer.OfferTags : System.Collections.Generic.IReadOnlyList +public readonly AdaptySDK.AdaptySubscriptionOffer.Phases : System.Collections.Generic.IReadOnlyList +public readonly AdaptySDK.AdaptySubscriptionOffer.Type : AdaptySDK.AdaptySubscriptionOfferType +public readonly AdaptySDK.AdaptySubscriptionPeriod.NumberOfUnits : System.Int64 +public readonly AdaptySDK.AdaptySubscriptionPeriod.Unit : AdaptySDK.AdaptySubscriptionPeriodUnit +public readonly AdaptySDK.AdaptySubscriptionPhase.LocalizedNumberOfPeriods : System.String +public readonly AdaptySDK.AdaptySubscriptionPhase.LocalizedSubscriptionPeriod : System.String +public readonly AdaptySDK.AdaptySubscriptionPhase.NumberOfPeriods : System.Int32 +public readonly AdaptySDK.AdaptySubscriptionPhase.PaymentMode : AdaptySDK.AdaptyPaymentMode +public readonly AdaptySDK.AdaptySubscriptionPhase.Price : AdaptySDK.AdaptyPrice +public readonly AdaptySDK.AdaptySubscriptionPhase.SubscriptionPeriod : AdaptySDK.AdaptySubscriptionPeriod +public readonly AdaptySDK.AdaptyUIOnboardingMeta.OnboardingId : System.String +public readonly AdaptySDK.AdaptyUIOnboardingMeta.ScreenClientId : System.String +public readonly AdaptySDK.AdaptyUIOnboardingMeta.ScreenIndex : System.Int32 +public readonly AdaptySDK.AdaptyUIOnboardingMeta.ScreensTotal : System.Int32 +public sealed class AdaptySDK.AdaptyConfiguration +public sealed class AdaptySDK.AdaptyConfiguration+Builder +public sealed class AdaptySDK.AdaptyCustomAssetColor : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLinearGradient : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalImageAsset : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalImageData : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalImageFile : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalVideoAsset : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalVideoFile : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomerIdentity +public sealed class AdaptySDK.AdaptyError +public sealed class AdaptySDK.AdaptyFlow +public sealed class AdaptySDK.AdaptyFlowPaywall +public sealed class AdaptySDK.AdaptyInstallationDetails +public sealed class AdaptySDK.AdaptyInstallationStatus +public sealed class AdaptySDK.AdaptyOnboarding +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingCompleted : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingStarted : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventProductsScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventRegistrationScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventSecondScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventUnknown : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventUserEmailCollected : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsDatePickerParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsEmailInput : AdaptySDK.AdaptyOnboardingsInput +public sealed class AdaptySDK.AdaptyOnboardingsInputParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsMultiSelectParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsNumberInput : AdaptySDK.AdaptyOnboardingsInput +public sealed class AdaptySDK.AdaptyOnboardingsSelectParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsTextInput : AdaptySDK.AdaptyOnboardingsInput +public sealed class AdaptySDK.AdaptyPaywallProduct +public sealed class AdaptySDK.AdaptyPlacement +public sealed class AdaptySDK.AdaptyPlacementFetchPolicy +public sealed class AdaptySDK.AdaptyPrice +public sealed class AdaptySDK.AdaptyProductIdentifier +public sealed class AdaptySDK.AdaptyProfile +public sealed class AdaptySDK.AdaptyProfile+AccessLevel +public sealed class AdaptySDK.AdaptyProfile+NonSubscription +public sealed class AdaptySDK.AdaptyProfile+Subscription +public sealed class AdaptySDK.AdaptyProfileParameters +public sealed class AdaptySDK.AdaptyProfileParameters+Builder +public sealed class AdaptySDK.AdaptyPurchaseParameters +public sealed class AdaptySDK.AdaptyPurchaseParametersBuilder +public sealed class AdaptySDK.AdaptyPurchaseResult +public sealed class AdaptySDK.AdaptyRemoteConfig +public sealed class AdaptySDK.AdaptySubscription +public sealed class AdaptySDK.AdaptySubscriptionOffer +public sealed class AdaptySDK.AdaptySubscriptionPeriod +public sealed class AdaptySDK.AdaptySubscriptionPhase +public sealed class AdaptySDK.AdaptySubscriptionUpdateParameters +public sealed class AdaptySDK.AdaptyUICreateFlowViewParameters +public sealed class AdaptySDK.AdaptyUIDialogConfiguration +public sealed class AdaptySDK.AdaptyUIFlowView +public sealed class AdaptySDK.AdaptyUIMediaCacheConfiguration +public sealed class AdaptySDK.AdaptyUIOnboardingMeta +public sealed class AdaptySDK.AdaptyUIOnboardingView +public sealed class AdaptySDK.AdaptyUIUserAction +public static AdaptySDK.Adapty.Activate(AdaptySDK.AdaptyConfiguration configuration, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Activate(AdaptySDK.Builder configurationBuilder, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.CreateWebPaywallUrl(AdaptySDK.AdaptyFlowPaywall paywall, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.CreateWebPaywallUrl(AdaptySDK.AdaptyPaywallProduct product, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetCurrentInstallationStatus(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlow(System.String placementId, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Nullable loadTimeout, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlow(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlowForDefaultAudience(System.String placementId, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlowForDefaultAudience(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetLogLevel(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetNativeSDKVersion(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboarding(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboarding(System.String placementId, System.String locale, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Nullable loadTimeout, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, System.String locale, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, System.String locale, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetPaywallProducts(AdaptySDK.AdaptyFlow flow, System.Action, AdaptySDK.AdaptyError> completionHandler) : System.Void +public static AdaptySDK.Adapty.GetProfile(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Identify(System.String customerUserId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Identify(System.String customerUserId, System.Guid iosAppAccountToken, System.String androidObfuscatedAccountId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.IsActivated(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.LogShowFlow(AdaptySDK.AdaptyFlow flow, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Logout(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.MakePurchase(AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyPurchaseParameters purchaseParameters, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.MakePurchase(AdaptySDK.AdaptyPaywallProduct product, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyFlowPaywall paywall, AdaptySDK.AdaptyWebPresentation openIn, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyFlowPaywall paywall, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyWebPresentation openIn, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyPaywallProduct product, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.PresentCodeRedemptionSheet(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.ReportTransaction(System.String transactionId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.ReportTransaction(System.String transactionId, System.String variationId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.RestorePurchases(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetEventListener(AdaptySDK.IAdaptyEventListener listener) : System.Void +public static AdaptySDK.Adapty.SetFallback(System.String fileName, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetFlowsEventsListener(AdaptySDK.IAdaptyFlowsEventsListener listener) : System.Void +public static AdaptySDK.Adapty.SetIntegrationIdentifier(System.String key, System.String value, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetLogLevel(AdaptySDK.AdaptyLogLevel level, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetObserverModeResolver(AdaptySDK.IAdaptyUIObserverModeResolver resolver) : System.Void +public static AdaptySDK.Adapty.SetOnboardingsEventsListener(AdaptySDK.IAdaptyOnboardingsEventsListener listener) : System.Void +public static AdaptySDK.Adapty.SetSystemRequestsHandler(AdaptySDK.IAdaptyUISystemRequestsHandler handler) : System.Void +public static AdaptySDK.Adapty.UpdateAppStoreCollectingRefundDataConsent(System.Boolean consent, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateAppStoreRefundPreference(AdaptySDK.AdaptyRefundPreference refundPreference, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateAttribution(System.Collections.Generic.IReadOnlyDictionary attribution, System.String source, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateAttribution(System.String jsonString, System.String source, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateProfile(AdaptySDK.AdaptyProfileParameters param, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyCustomAsset.Color(UnityEngine.Color color) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LinearGradient(UnityEngine.Gradient gradient) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalImageAsset(System.String assetId) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalImageData(System.Byte[] data) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalImageFile(System.String path) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalVideoAsset(System.String assetId) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalVideoFile(System.String path) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyPlacementFetchPolicy.ReturnCacheDataIfNotExpiredElseLoad(System.TimeSpan maxAge) : AdaptySDK.AdaptyPlacementFetchPolicy +public static AdaptySDK.AdaptyUI.CreateFlowView(AdaptySDK.AdaptyFlow flow, AdaptySDK.AdaptyUICreateFlowViewParameters optionalParameters, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.CreateFlowView(AdaptySDK.AdaptyFlow flow, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.CreateOnboardingView(AdaptySDK.AdaptyOnboarding onboarding, AdaptySDK.AdaptyWebPresentation externalUrlsPresentation, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.CreateOnboardingView(AdaptySDK.AdaptyOnboarding onboarding, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.DismissFlowView(AdaptySDK.AdaptyUIFlowView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.DismissOnboardingView(AdaptySDK.AdaptyUIOnboardingView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.OpenUrl(System.String url, AdaptySDK.AdaptyWebPresentation openIn, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentFlowView(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentFlowView(AdaptySDK.AdaptyUIFlowView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentOnboardingView(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentOnboardingView(AdaptySDK.AdaptyUIOnboardingView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.RequestAppReview(System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.ShowDialog(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyUIDialogConfiguration configuration, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.ShowDialog(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIDialogConfiguration configuration, System.Action completionHandler) : System.Void +public static class AdaptySDK.Adapty +public static class AdaptySDK.AdaptyUI +public static const AdaptySDK.AdaptyErrorCode.ActivateOnceError : AdaptySDK.AdaptyErrorCode = 3005 +public static const AdaptySDK.AdaptyErrorCode.AdaptyNotInitialized : AdaptySDK.AdaptyErrorCode = 20 +public static const AdaptySDK.AdaptyErrorCode.AnalyticsDisabled : AdaptySDK.AdaptyErrorCode = 3000 +public static const AdaptySDK.AdaptyErrorCode.BadRequest : AdaptySDK.AdaptyErrorCode = 2003 +public static const AdaptySDK.AdaptyErrorCode.BillingError : AdaptySDK.AdaptyErrorCode = 106 +public static const AdaptySDK.AdaptyErrorCode.BillingNetworkError : AdaptySDK.AdaptyErrorCode = 112 +public static const AdaptySDK.AdaptyErrorCode.BillingServiceDisconnected : AdaptySDK.AdaptyErrorCode = 99 +public static const AdaptySDK.AdaptyErrorCode.BillingServiceTimeout : AdaptySDK.AdaptyErrorCode = 97 +public static const AdaptySDK.AdaptyErrorCode.BillingServiceUnavailable : AdaptySDK.AdaptyErrorCode = 102 +public static const AdaptySDK.AdaptyErrorCode.BillingUnavailable : AdaptySDK.AdaptyErrorCode = 103 +public static const AdaptySDK.AdaptyErrorCode.CantMakePayments : AdaptySDK.AdaptyErrorCode = 1003 +public static const AdaptySDK.AdaptyErrorCode.CantReadReceipt : AdaptySDK.AdaptyErrorCode = 1005 +public static const AdaptySDK.AdaptyErrorCode.ClientInvalid : AdaptySDK.AdaptyErrorCode = 1 +public static const AdaptySDK.AdaptyErrorCode.CloudServiceNetworkConnectionFailed : AdaptySDK.AdaptyErrorCode = 7 +public static const AdaptySDK.AdaptyErrorCode.CloudServicePermissionDenied : AdaptySDK.AdaptyErrorCode = 6 +public static const AdaptySDK.AdaptyErrorCode.CloudServiceRevoked : AdaptySDK.AdaptyErrorCode = 8 +public static const AdaptySDK.AdaptyErrorCode.CurrentSubscriptionToUpdateNotFoundInHistory : AdaptySDK.AdaptyErrorCode = 24 +public static const AdaptySDK.AdaptyErrorCode.DecodingFailed : AdaptySDK.AdaptyErrorCode = 2006 +public static const AdaptySDK.AdaptyErrorCode.DeveloperError : AdaptySDK.AdaptyErrorCode = 105 +public static const AdaptySDK.AdaptyErrorCode.EncodingFailed : AdaptySDK.AdaptyErrorCode = 2009 +public static const AdaptySDK.AdaptyErrorCode.FeatureNotSupported : AdaptySDK.AdaptyErrorCode = 98 +public static const AdaptySDK.AdaptyErrorCode.FetchSubscriptionStatusFailed : AdaptySDK.AdaptyErrorCode = 1020 +public static const AdaptySDK.AdaptyErrorCode.FetchTimeoutError : AdaptySDK.AdaptyErrorCode = 3101 +public static const AdaptySDK.AdaptyErrorCode.InvalidActionUrl : AdaptySDK.AdaptyErrorCode = 4107 +public static const AdaptySDK.AdaptyErrorCode.InvalidOfferIdentifier : AdaptySDK.AdaptyErrorCode = 11 +public static const AdaptySDK.AdaptyErrorCode.InvalidOfferPrice : AdaptySDK.AdaptyErrorCode = 14 +public static const AdaptySDK.AdaptyErrorCode.InvalidSignature : AdaptySDK.AdaptyErrorCode = 12 +public static const AdaptySDK.AdaptyErrorCode.ItemAlreadyOwned : AdaptySDK.AdaptyErrorCode = 107 +public static const AdaptySDK.AdaptyErrorCode.ItemNotOwned : AdaptySDK.AdaptyErrorCode = 108 +public static const AdaptySDK.AdaptyErrorCode.JsException : AdaptySDK.AdaptyErrorCode = 4105 +public static const AdaptySDK.AdaptyErrorCode.MissingOfferParams : AdaptySDK.AdaptyErrorCode = 13 +public static const AdaptySDK.AdaptyErrorCode.NavigatorNotFound : AdaptySDK.AdaptyErrorCode = 4106 +public static const AdaptySDK.AdaptyErrorCode.NetworkFailed : AdaptySDK.AdaptyErrorCode = 2005 +public static const AdaptySDK.AdaptyErrorCode.NoProductIDsFound : AdaptySDK.AdaptyErrorCode = 1000 +public static const AdaptySDK.AdaptyErrorCode.NoPurchasesToRestore : AdaptySDK.AdaptyErrorCode = 1004 +public static const AdaptySDK.AdaptyErrorCode.NotActivated : AdaptySDK.AdaptyErrorCode = 2002 +public static const AdaptySDK.AdaptyErrorCode.OperationInterrupted : AdaptySDK.AdaptyErrorCode = 9000 +public static const AdaptySDK.AdaptyErrorCode.PaymentCancelled : AdaptySDK.AdaptyErrorCode = 2 +public static const AdaptySDK.AdaptyErrorCode.PaymentInvalid : AdaptySDK.AdaptyErrorCode = 3 +public static const AdaptySDK.AdaptyErrorCode.PaymentNotAllowed : AdaptySDK.AdaptyErrorCode = 4 +public static const AdaptySDK.AdaptyErrorCode.PaymentPendingError : AdaptySDK.AdaptyErrorCode = 1050 +public static const AdaptySDK.AdaptyErrorCode.PrivacyAcknowledgementRequired : AdaptySDK.AdaptyErrorCode = 9 +public static const AdaptySDK.AdaptyErrorCode.ProductNotFound : AdaptySDK.AdaptyErrorCode = 22 +public static const AdaptySDK.AdaptyErrorCode.ProductPurchaseFailed : AdaptySDK.AdaptyErrorCode = 1006 +public static const AdaptySDK.AdaptyErrorCode.ProductRequestFailed : AdaptySDK.AdaptyErrorCode = 1002 +public static const AdaptySDK.AdaptyErrorCode.ProfileWasChanged : AdaptySDK.AdaptyErrorCode = 3006 +public static const AdaptySDK.AdaptyErrorCode.RefreshReceiptFailed : AdaptySDK.AdaptyErrorCode = 1010 +public static const AdaptySDK.AdaptyErrorCode.ServerError : AdaptySDK.AdaptyErrorCode = 2004 +public static const AdaptySDK.AdaptyErrorCode.StoreProductNotAvailable : AdaptySDK.AdaptyErrorCode = 5 +public static const AdaptySDK.AdaptyErrorCode.UnauthorizedRequestData : AdaptySDK.AdaptyErrorCode = 10 +public static const AdaptySDK.AdaptyErrorCode.UnidentifiedUserLogout : AdaptySDK.AdaptyErrorCode = 3020 +public static const AdaptySDK.AdaptyErrorCode.Unknown : AdaptySDK.AdaptyErrorCode = 0 +public static const AdaptySDK.AdaptyErrorCode.UnsupportedData : AdaptySDK.AdaptyErrorCode = 3007 +public static const AdaptySDK.AdaptyErrorCode.WrongAssetType : AdaptySDK.AdaptyErrorCode = 4104 +public static const AdaptySDK.AdaptyErrorCode.WrongParam : AdaptySDK.AdaptyErrorCode = 3001 +public static const AdaptySDK.AdaptyInstallationStatusType.Determined : AdaptySDK.AdaptyInstallationStatusType = 2 +public static const AdaptySDK.AdaptyInstallationStatusType.NotAvailable : AdaptySDK.AdaptyInstallationStatusType = 0 +public static const AdaptySDK.AdaptyInstallationStatusType.NotDetermined : AdaptySDK.AdaptyInstallationStatusType = 1 +public static const AdaptySDK.AdaptyLogLevel.Debug : AdaptySDK.AdaptyLogLevel = 4 +public static const AdaptySDK.AdaptyLogLevel.Error : AdaptySDK.AdaptyLogLevel = 0 +public static const AdaptySDK.AdaptyLogLevel.Info : AdaptySDK.AdaptyLogLevel = 2 +public static const AdaptySDK.AdaptyLogLevel.Verbose : AdaptySDK.AdaptyLogLevel = 3 +public static const AdaptySDK.AdaptyLogLevel.Warn : AdaptySDK.AdaptyLogLevel = 1 +public static const AdaptySDK.AdaptyPaymentMode.FreeTrial : AdaptySDK.AdaptyPaymentMode = 2 +public static const AdaptySDK.AdaptyPaymentMode.PayAsYouGo : AdaptySDK.AdaptyPaymentMode = 0 +public static const AdaptySDK.AdaptyPaymentMode.PayUpFront : AdaptySDK.AdaptyPaymentMode = 1 +public static const AdaptySDK.AdaptyPaymentMode.Unknown : AdaptySDK.AdaptyPaymentMode = 3 +public static const AdaptySDK.AdaptyProfileGender.Female : AdaptySDK.AdaptyProfileGender = 0 +public static const AdaptySDK.AdaptyProfileGender.Male : AdaptySDK.AdaptyProfileGender = 1 +public static const AdaptySDK.AdaptyProfileGender.Other : AdaptySDK.AdaptyProfileGender = 2 +public static const AdaptySDK.AdaptyPurchaseResultType.Pending : AdaptySDK.AdaptyPurchaseResultType = 0 +public static const AdaptySDK.AdaptyPurchaseResultType.Success : AdaptySDK.AdaptyPurchaseResultType = 2 +public static const AdaptySDK.AdaptyPurchaseResultType.UserCancelled : AdaptySDK.AdaptyPurchaseResultType = 1 +public static const AdaptySDK.AdaptyRefundPreference.Decline : AdaptySDK.AdaptyRefundPreference = 2 +public static const AdaptySDK.AdaptyRefundPreference.Grant : AdaptySDK.AdaptyRefundPreference = 1 +public static const AdaptySDK.AdaptyRefundPreference.NoPreference : AdaptySDK.AdaptyRefundPreference = 0 +public static const AdaptySDK.AdaptyServerCluster.CN : AdaptySDK.AdaptyServerCluster = 2 +public static const AdaptySDK.AdaptyServerCluster.Default : AdaptySDK.AdaptyServerCluster = 0 +public static const AdaptySDK.AdaptyServerCluster.EU : AdaptySDK.AdaptyServerCluster = 1 +public static const AdaptySDK.AdaptySubscriptionOfferType.Code : AdaptySDK.AdaptySubscriptionOfferType = 3 +public static const AdaptySDK.AdaptySubscriptionOfferType.Introductory : AdaptySDK.AdaptySubscriptionOfferType = 0 +public static const AdaptySDK.AdaptySubscriptionOfferType.Promotional : AdaptySDK.AdaptySubscriptionOfferType = 1 +public static const AdaptySDK.AdaptySubscriptionOfferType.WinBack : AdaptySDK.AdaptySubscriptionOfferType = 2 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Day : AdaptySDK.AdaptySubscriptionPeriodUnit = 0 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Month : AdaptySDK.AdaptySubscriptionPeriodUnit = 2 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Unknown : AdaptySDK.AdaptySubscriptionPeriodUnit = 4 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Week : AdaptySDK.AdaptySubscriptionPeriodUnit = 1 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Year : AdaptySDK.AdaptySubscriptionPeriodUnit = 3 +public static const AdaptySDK.AdaptySubscriptionRenewalType.Autorenewable : AdaptySDK.AdaptySubscriptionRenewalType = 1 +public static const AdaptySDK.AdaptySubscriptionRenewalType.Prepaid : AdaptySDK.AdaptySubscriptionRenewalType = 0 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.ChargeFullPrice : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 4 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.ChargeProratedPrice : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 1 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.Deferred : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 3 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.WithTimeProration : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 0 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.WithoutProration : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 2 +public static const AdaptySDK.AdaptyUIDialogActionType.Primary : AdaptySDK.AdaptyUIDialogActionType = 0 +public static const AdaptySDK.AdaptyUIDialogActionType.Secondary : AdaptySDK.AdaptyUIDialogActionType = 1 +public static const AdaptySDK.AdaptyUIIOSPresentationStyle.FullScreen : AdaptySDK.AdaptyUIIOSPresentationStyle = 0 +public static const AdaptySDK.AdaptyUIIOSPresentationStyle.PageSheet : AdaptySDK.AdaptyUIIOSPresentationStyle = 1 +public static const AdaptySDK.AdaptyUIUserActionType.Close : AdaptySDK.AdaptyUIUserActionType = 0 +public static const AdaptySDK.AdaptyUIUserActionType.Custom : AdaptySDK.AdaptyUIUserActionType = 3 +public static const AdaptySDK.AdaptyUIUserActionType.OpenUrl : AdaptySDK.AdaptyUIUserActionType = 2 +public static const AdaptySDK.AdaptyUIUserActionType.SystemBack : AdaptySDK.AdaptyUIUserActionType = 1 +public static const AdaptySDK.AdaptyWebPresentation.ExternalBrowser : AdaptySDK.AdaptyWebPresentation = 0 +public static const AdaptySDK.AdaptyWebPresentation.InAppBrowser : AdaptySDK.AdaptyWebPresentation = 1 +public static const AdaptySDK.AppTrackingTransparencyStatus.Authorized : AdaptySDK.AppTrackingTransparencyStatus = 3 +public static const AdaptySDK.AppTrackingTransparencyStatus.Denied : AdaptySDK.AppTrackingTransparencyStatus = 2 +public static const AdaptySDK.AppTrackingTransparencyStatus.NotDetermined : AdaptySDK.AppTrackingTransparencyStatus = 0 +public static const AdaptySDK.AppTrackingTransparencyStatus.Restricted : AdaptySDK.AppTrackingTransparencyStatus = 1 +public static readonly AdaptySDK.Adapty.SDKVersion : System.String +public static readonly AdaptySDK.AdaptyPlacementFetchPolicy.Default : AdaptySDK.AdaptyPlacementFetchPolicy +public static readonly AdaptySDK.AdaptyPlacementFetchPolicy.ReloadRevalidatingCacheData : AdaptySDK.AdaptyPlacementFetchPolicy +public static readonly AdaptySDK.AdaptyPlacementFetchPolicy.ReturnCacheDataElseLoad : AdaptySDK.AdaptyPlacementFetchPolicy \ No newline at end of file diff --git a/tests/shared/Fixtures/approved/public-surface.editor.approved.txt b/tests/shared/Fixtures/approved/public-surface.editor.approved.txt new file mode 100644 index 0000000..d2e1cb7 --- /dev/null +++ b/tests/shared/Fixtures/approved/public-surface.editor.approved.txt @@ -0,0 +1,653 @@ +protected AdaptySDK.AdaptyCustomAsset.ctor() +protected AdaptySDK.AdaptyOnboardingsAnalyticsEvent.ctor() +protected AdaptySDK.AdaptyOnboardingsInput.ctor() +protected AdaptySDK.AdaptyOnboardingsStateUpdatedParams.ctor() +public AdaptySDK.AdaptyConfiguration+Builder.ActivateUI : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.AdaptyUIMediaCache : AdaptySDK.AdaptyUIMediaCacheConfiguration +public AdaptySDK.AdaptyConfiguration+Builder.ApiKey : System.String +public AdaptySDK.AdaptyConfiguration+Builder.AppleClearDataOnBackup : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.AppleIdfaCollectionDisabled : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.BackendProxyHost : System.String +public AdaptySDK.AdaptyConfiguration+Builder.BackendProxyPort : System.Int32 +public AdaptySDK.AdaptyConfiguration+Builder.Build() : AdaptySDK.AdaptyConfiguration +public AdaptySDK.AdaptyConfiguration+Builder.CustomerIdentity : AdaptySDK.AdaptyCustomerIdentity +public AdaptySDK.AdaptyConfiguration+Builder.CustomerUserId : System.String +public AdaptySDK.AdaptyConfiguration+Builder.GoogleAdvertisingIdCollectionDisabled : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.GoogleEnablePendingPrepaidPlans : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.GoogleLocalAccessLevelAllowed : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.IpAddressCollectionDisabled : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.LogLevel : AdaptySDK.AdaptyLogLevel +public AdaptySDK.AdaptyConfiguration+Builder.ObserverMode : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.ServerCluster : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.SetAPIKey(System.String apiKey) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetActivateUI(System.Boolean activate) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetAdaptyUIMediaCache(System.Nullable memoryStorageTotalCostLimit, System.Nullable memoryStorageCountLimit, System.Nullable diskStorageSizeLimit) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetAppleClearDataOnBackup(System.Boolean appleClearDataOnBackup) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetAppleIDFACollectionDisabled(System.Boolean appleIdfaCollectionDisabled) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetBackendProxy(System.String host, System.Int32 port) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetCustomerUserId(System.String customerUserId) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetCustomerUserId(System.String customerUserId, System.Guid iosAppAccountToken, System.String androidObfuscatedAccountId) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetGoogleAdvertisingIdCollectionDisabled(System.Boolean googleAdvertisingIdCollectionDisabled) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetGoogleEnablePendingPrepaidPlans(System.Boolean googleEnablePendingPrepaidPlans) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetGoogleLocalAccessLevelAllowed(System.Boolean googleLocalAccessLevelAllowed) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetIPAddressCollectionDisabled(System.Boolean ipAddressCollectionDisabled) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetObserverMode(System.Boolean observerMode) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetServerCluster(AdaptySDK.AdaptyServerCluster serverCluster) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.ctor(System.String apiKey) +public AdaptySDK.AdaptyCustomAssetColor.ColorValue : UnityEngine.Color { get; } +public AdaptySDK.AdaptyCustomAssetLinearGradient.Gradient : UnityEngine.Gradient { get; } +public AdaptySDK.AdaptyCustomAssetLocalImageAsset.AssetId : System.String { get; } +public AdaptySDK.AdaptyCustomAssetLocalImageData.Data : System.Byte[] { get; } +public AdaptySDK.AdaptyCustomAssetLocalImageFile.Path : System.String { get; } +public AdaptySDK.AdaptyCustomAssetLocalVideoAsset.AssetId : System.String { get; } +public AdaptySDK.AdaptyCustomAssetLocalVideoFile.Path : System.String { get; } +public AdaptySDK.AdaptyCustomerIdentity.IsEmpty : System.Boolean { get; } +public AdaptySDK.AdaptyCustomerIdentity.ctor(System.Guid iosAppAccountToken, System.String androidObfuscatedAccountId) +public AdaptySDK.AdaptyErrorCode.value__ : System.Int32 +public AdaptySDK.AdaptyFlow.Paywalls : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlow.ProductIdentifiers : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlow.RemoteConfig : AdaptySDK.AdaptyRemoteConfig { get; } +public AdaptySDK.AdaptyFlow.RemoteConfigs : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlow.VendorProductIds : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlowPaywall.ProductIdentifiers : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlowPaywall.VendorProductIds : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyInstallationStatus.Details : AdaptySDK.AdaptyInstallationDetails { get; } +public AdaptySDK.AdaptyInstallationStatusType.value__ : System.Int32 +public AdaptySDK.AdaptyLogLevel.value__ : System.Int32 +public AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingCompleted.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingStarted.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventProductsScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventRegistrationScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted.ctor(System.String elementId, System.String reply) +public AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventSecondScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventUnknown.ctor(System.String name) +public AdaptySDK.AdaptyOnboardingsAnalyticsEventUserEmailCollected.ctor() +public AdaptySDK.AdaptyOnboardingsDatePickerParams.ctor(System.Nullable day, System.Nullable month, System.Nullable year) +public AdaptySDK.AdaptyOnboardingsEmailInput.ctor(System.String value) +public AdaptySDK.AdaptyOnboardingsInputParams.ctor(AdaptySDK.AdaptyOnboardingsInput input) +public AdaptySDK.AdaptyOnboardingsMultiSelectParams.ctor(System.Collections.Generic.IList params) +public AdaptySDK.AdaptyOnboardingsNumberInput.ctor(System.Double value) +public AdaptySDK.AdaptyOnboardingsSelectParams.ctor(System.String id, System.String value, System.String label) +public AdaptySDK.AdaptyOnboardingsTextInput.ctor(System.String value) +public AdaptySDK.AdaptyPaymentMode.value__ : System.Int32 +public AdaptySDK.AdaptyProductIdentifier.ctor(System.String vendorProductId, System.String adaptyProductId, System.String basePlanId) +public AdaptySDK.AdaptyProfile.AccessLevels : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfile.AppliedAttributionSources : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyProfile.CustomAttributes : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfile.NonSubscriptions : System.Collections.Generic.IReadOnlyDictionary> { get; } +public AdaptySDK.AdaptyProfile.Subscriptions : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfileGender.value__ : System.Int32 +public AdaptySDK.AdaptyProfileParameters+Builder.Build() : AdaptySDK.AdaptyProfileParameters +public AdaptySDK.AdaptyProfileParameters+Builder.RemoveCustomAttribute(System.String key) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetAnalyticsDisabled(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetAppTrackingTransparencyStatus(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetBirthday(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetCustomDoubleAttribute(System.String key, System.Double value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetCustomStringAttribute(System.String key, System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetEmail(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetFirstName(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetGender(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetLastName(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetPhoneNumber(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.ctor() +public AdaptySDK.AdaptyProfileParameters.AnalyticsDisabled : System.Nullable +public AdaptySDK.AdaptyProfileParameters.AppTrackingTransparencyStatus : System.Nullable +public AdaptySDK.AdaptyProfileParameters.Birthday : System.Nullable +public AdaptySDK.AdaptyProfileParameters.CustomAttributes : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfileParameters.Email : System.String +public AdaptySDK.AdaptyProfileParameters.FirstName : System.String +public AdaptySDK.AdaptyProfileParameters.Gender : System.Nullable +public AdaptySDK.AdaptyProfileParameters.LastName : System.String +public AdaptySDK.AdaptyProfileParameters.PhoneNumber : System.String +public AdaptySDK.AdaptyProfileParameters.RemoveCustomAttribute(System.String key) : System.Void +public AdaptySDK.AdaptyProfileParameters.SetCustomDoubleAttribute(System.String key, System.Double value) : System.Void +public AdaptySDK.AdaptyProfileParameters.SetCustomStringAttribute(System.String key, System.String value) : System.Void +public AdaptySDK.AdaptyProfileParameters.ctor() +public AdaptySDK.AdaptyPurchaseParameters.ctor(AdaptySDK.AdaptySubscriptionUpdateParameters subscriptionUpdateParams = null, System.Nullable isOfferPersonalized = null) +public AdaptySDK.AdaptyPurchaseParametersBuilder.Build() : AdaptySDK.AdaptyPurchaseParameters +public AdaptySDK.AdaptyPurchaseParametersBuilder.SetIsOfferPersonalized(System.Nullable isOfferPersonalized) : AdaptySDK.AdaptyPurchaseParametersBuilder +public AdaptySDK.AdaptyPurchaseParametersBuilder.SetSubscriptionUpdateParams(AdaptySDK.AdaptySubscriptionUpdateParameters subscriptionUpdateParams) : AdaptySDK.AdaptyPurchaseParametersBuilder +public AdaptySDK.AdaptyPurchaseParametersBuilder.ctor() +public AdaptySDK.AdaptyPurchaseResultType.value__ : System.Int32 +public AdaptySDK.AdaptyRefundPreference.value__ : System.Int32 +public AdaptySDK.AdaptyRemoteConfig.Dictionary : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyServerCluster.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionOfferType.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionPeriodUnit.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionRenewalType.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionUpdateParameters.OldSubVendorProductId : System.String +public AdaptySDK.AdaptySubscriptionUpdateParameters.ReplacementMode : AdaptySDK.AdaptySubscriptionUpdateReplacementMode +public AdaptySDK.AdaptySubscriptionUpdateParameters.ctor(System.String oldSubVendorProductId, AdaptySDK.AdaptySubscriptionUpdateReplacementMode replacementMode) +public AdaptySDK.AdaptySubscriptionUpdateReplacementMode.value__ : System.Int32 +public AdaptySDK.AdaptyUICreateFlowViewParameters.CustomAssets : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.CustomTags : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.CustomTimers : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.EnableSafeAreaPaddings : System.Nullable +public AdaptySDK.AdaptyUICreateFlowViewParameters.LoadTimeout : System.Nullable +public AdaptySDK.AdaptyUICreateFlowViewParameters.Locale : System.String +public AdaptySDK.AdaptyUICreateFlowViewParameters.PreloadProducts : System.Nullable +public AdaptySDK.AdaptyUICreateFlowViewParameters.ProductPurchaseParameters : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetCustomAssets(System.Collections.Generic.IReadOnlyDictionary customAssets) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetCustomTags(System.Collections.Generic.IReadOnlyDictionary customTags) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetCustomTimers(System.Collections.Generic.IReadOnlyDictionary customTimers) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetEnableSafeAreaPaddings(System.Nullable enableSafeAreaPaddings) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetLoadTimeout(System.Nullable loadTimeout) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetLocale(System.String locale) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetPreloadProducts(System.Nullable preloadProducts) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetProductPurchaseParameters(System.Collections.Generic.IReadOnlyDictionary productPurchaseParameters) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.ctor() +public AdaptySDK.AdaptyUIDialogActionType.value__ : System.Int32 +public AdaptySDK.AdaptyUIDialogConfiguration.Content : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.DefaultActionTitle : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.SecondaryActionTitle : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.SetContent(System.String content) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.SetDefaultActionTitle(System.String defaultActionTitle) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.SetSecondaryActionTitle(System.String secondaryActionTitle) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.SetTitle(System.String title) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.Title : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.ctor() +public AdaptySDK.AdaptyUIFlowView.Dismiss(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIFlowView.Id : System.String +public AdaptySDK.AdaptyUIFlowView.Locale : System.String +public AdaptySDK.AdaptyUIFlowView.PlacementId : System.String +public AdaptySDK.AdaptyUIFlowView.Present(AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIFlowView.Present(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIFlowView.VariationId : System.String +public AdaptySDK.AdaptyUIIOSPresentationStyle.value__ : System.Int32 +public AdaptySDK.AdaptyUIMediaCacheConfiguration.DiskStorageSizeLimit : System.Nullable +public AdaptySDK.AdaptyUIMediaCacheConfiguration.MemoryStorageCountLimit : System.Nullable +public AdaptySDK.AdaptyUIMediaCacheConfiguration.MemoryStorageTotalCostLimit : System.Nullable +public AdaptySDK.AdaptyUIMediaCacheConfiguration.ctor(System.Nullable memoryStorageTotalCostLimit, System.Nullable memoryStorageCountLimit, System.Nullable diskStorageSizeLimit) +public AdaptySDK.AdaptyUIOnboardingView.Dismiss(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIOnboardingView.Id : System.String +public AdaptySDK.AdaptyUIOnboardingView.PaywallVariationId : System.String +public AdaptySDK.AdaptyUIOnboardingView.PlacementId : System.String +public AdaptySDK.AdaptyUIOnboardingView.Present(AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIOnboardingView.Present(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIUserAction.OpenIn : System.Nullable +public AdaptySDK.AdaptyUIUserAction.Type : AdaptySDK.AdaptyUIUserActionType +public AdaptySDK.AdaptyUIUserAction.Value : System.String +public AdaptySDK.AdaptyUIUserActionType.value__ : System.Int32 +public AdaptySDK.AdaptyWebPresentation.value__ : System.Int32 +public AdaptySDK.AppTrackingTransparencyStatus.value__ : System.Int32 +public abstract AdaptySDK.IAdaptyEventListener.OnInstallationDetailsFail(AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyEventListener.OnInstallationDetailsSuccess(AdaptySDK.AdaptyInstallationDetails details) : System.Void +public abstract AdaptySDK.IAdaptyEventListener.OnLoadLatestProfile(AdaptySDK.AdaptyProfile profile) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidAppear(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidDisappear(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFailLoadingProducts(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFailPurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFailRestore(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFinishPurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyPurchaseResult purchasedResult) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFinishRestore(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyProfile profile) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFinishWebPaymentNavigation(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidPerformAction(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyUIUserAction action) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidReceiveAnalyticEvent(AdaptySDK.AdaptyUIFlowView view, System.String name, System.Collections.Generic.IReadOnlyDictionary parameters) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidReceiveError(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidSelectProduct(AdaptySDK.AdaptyUIFlowView view, System.String productId) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidStartPurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidStartRestore(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewDidFailWithError(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewDidFinishLoading(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnAnalyticsEvent(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, AdaptySDK.AdaptyOnboardingsAnalyticsEvent analyticsEvent) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnCloseAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String actionId) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnCustomAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String actionId) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnPaywallAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String actionId) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnStateUpdatedAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String elementId, AdaptySDK.AdaptyOnboardingsStateUpdatedParams params) : System.Void +public abstract AdaptySDK.IAdaptyUIObserverModeResolver.FlowViewDidInitiatePurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, System.Action onStartPurchase, System.Action onFinishPurchase) : System.Void +public abstract AdaptySDK.IAdaptyUIObserverModeResolver.FlowViewDidInitiateRestore(AdaptySDK.AdaptyUIFlowView view, System.Action onStartRestore, System.Action onFinishRestore) : System.Void +public abstract AdaptySDK.IAdaptyUISystemRequestsHandler.FlowViewDidAskPermission(AdaptySDK.AdaptyUIFlowView view, System.String permission, System.Collections.Generic.IReadOnlyDictionary customArgs, System.Action respond) : System.Void +public abstract AdaptySDK.IAdaptyUISystemRequestsHandler.FlowViewDidRequestAppReview(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract class AdaptySDK.AdaptyCustomAsset +public abstract class AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public abstract class AdaptySDK.AdaptyOnboardingsInput +public abstract class AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public enum AdaptySDK.AdaptyErrorCode : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyInstallationStatusType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyLogLevel : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyPaymentMode : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyProfileGender : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyPurchaseResultType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyRefundPreference : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyServerCluster : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionOfferType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionPeriodUnit : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionRenewalType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionUpdateReplacementMode : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyUIDialogActionType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyUIIOSPresentationStyle : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyUIUserActionType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyWebPresentation : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AppTrackingTransparencyStatus : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public interface AdaptySDK.IAdaptyEventListener +public interface AdaptySDK.IAdaptyFlowsEventsListener +public interface AdaptySDK.IAdaptyOnboardingsEventsListener +public interface AdaptySDK.IAdaptyUIObserverModeResolver +public interface AdaptySDK.IAdaptyUISystemRequestsHandler +public override AdaptySDK.AdaptyConfiguration+Builder.ToString() : System.String +public override AdaptySDK.AdaptyConfiguration.ToString() : System.String +public override AdaptySDK.AdaptyCustomerIdentity.ToString() : System.String +public override AdaptySDK.AdaptyError.ToString() : System.String +public override AdaptySDK.AdaptyFlow.ToString() : System.String +public override AdaptySDK.AdaptyFlowPaywall.ToString() : System.String +public override AdaptySDK.AdaptyInstallationDetails.ToString() : System.String +public override AdaptySDK.AdaptyInstallationStatus.ToString() : System.String +public override AdaptySDK.AdaptyOnboarding.ToString() : System.String +public override AdaptySDK.AdaptyOnboardingsDatePickerParams.ToString() : System.String +public override AdaptySDK.AdaptyOnboardingsMultiSelectParams.ToString() : System.String +public override AdaptySDK.AdaptyOnboardingsSelectParams.ToString() : System.String +public override AdaptySDK.AdaptyPaywallProduct.ToString() : System.String +public override AdaptySDK.AdaptyPlacement.ToString() : System.String +public override AdaptySDK.AdaptyPlacementFetchPolicy.ToString() : System.String +public override AdaptySDK.AdaptyPrice.ToString() : System.String +public override AdaptySDK.AdaptyProductIdentifier.Equals(System.Object obj) : System.Boolean +public override AdaptySDK.AdaptyProductIdentifier.GetHashCode() : System.Int32 +public override AdaptySDK.AdaptyProductIdentifier.ToString() : System.String +public override AdaptySDK.AdaptyProfile+AccessLevel.ToString() : System.String +public override AdaptySDK.AdaptyProfile+NonSubscription.ToString() : System.String +public override AdaptySDK.AdaptyProfile+Subscription.ToString() : System.String +public override AdaptySDK.AdaptyProfile.ToString() : System.String +public override AdaptySDK.AdaptyProfileParameters.ToString() : System.String +public override AdaptySDK.AdaptyPurchaseParameters.ToString() : System.String +public override AdaptySDK.AdaptyPurchaseResult.ToString() : System.String +public override AdaptySDK.AdaptySubscription.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionOffer.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionPeriod.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionPhase.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionUpdateParameters.ToString() : System.String +public override AdaptySDK.AdaptyUICreateFlowViewParameters.ToString() : System.String +public override AdaptySDK.AdaptyUIDialogConfiguration.ToString() : System.String +public override AdaptySDK.AdaptyUIFlowView.ToString() : System.String +public override AdaptySDK.AdaptyUIMediaCacheConfiguration.ToString() : System.String +public override AdaptySDK.AdaptyUIOnboardingMeta.ToString() : System.String +public override AdaptySDK.AdaptyUIOnboardingView.ToString() : System.String +public override AdaptySDK.AdaptyUIUserAction.ToString() : System.String +public readonly AdaptySDK.AdaptyCustomerIdentity.AndroidObfuscatedAccountId : System.String +public readonly AdaptySDK.AdaptyCustomerIdentity.IosAppAccountToken : System.Guid +public readonly AdaptySDK.AdaptyError.Code : AdaptySDK.AdaptyErrorCode +public readonly AdaptySDK.AdaptyError.Detail : System.String +public readonly AdaptySDK.AdaptyError.Message : System.String +public readonly AdaptySDK.AdaptyFlow.FlowVersionId : System.String +public readonly AdaptySDK.AdaptyFlow.InstanceIdentity : System.String +public readonly AdaptySDK.AdaptyFlow.Name : System.String +public readonly AdaptySDK.AdaptyFlow.Placement : AdaptySDK.AdaptyPlacement +public readonly AdaptySDK.AdaptyFlow.VariationId : System.String +public readonly AdaptySDK.AdaptyFlowPaywall.InstanceIdentity : System.String +public readonly AdaptySDK.AdaptyFlowPaywall.Name : System.String +public readonly AdaptySDK.AdaptyFlowPaywall.Placement : AdaptySDK.AdaptyPlacement +public readonly AdaptySDK.AdaptyFlowPaywall.VariationId : System.String +public readonly AdaptySDK.AdaptyInstallationDetails.AppLaunchCount : System.Int32 +public readonly AdaptySDK.AdaptyInstallationDetails.InstallId : System.String +public readonly AdaptySDK.AdaptyInstallationDetails.InstallTime : System.DateTime +public readonly AdaptySDK.AdaptyInstallationDetails.Payload : System.String +public readonly AdaptySDK.AdaptyInstallationStatus.Status : AdaptySDK.AdaptyInstallationStatusType +public readonly AdaptySDK.AdaptyOnboarding.Name : System.String +public readonly AdaptySDK.AdaptyOnboarding.OnboardingId : System.String +public readonly AdaptySDK.AdaptyOnboarding.Placement : AdaptySDK.AdaptyPlacement +public readonly AdaptySDK.AdaptyOnboarding.RemoteConfig : AdaptySDK.AdaptyRemoteConfig +public readonly AdaptySDK.AdaptyOnboarding.VariationId : System.String +public readonly AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted.ElementId : System.String +public readonly AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted.Reply : System.String +public readonly AdaptySDK.AdaptyOnboardingsAnalyticsEventUnknown.Name : System.String +public readonly AdaptySDK.AdaptyOnboardingsDatePickerParams.Day : System.Nullable +public readonly AdaptySDK.AdaptyOnboardingsDatePickerParams.Month : System.Nullable +public readonly AdaptySDK.AdaptyOnboardingsDatePickerParams.Year : System.Nullable +public readonly AdaptySDK.AdaptyOnboardingsEmailInput.Value : System.String +public readonly AdaptySDK.AdaptyOnboardingsInputParams.Input : AdaptySDK.AdaptyOnboardingsInput +public readonly AdaptySDK.AdaptyOnboardingsMultiSelectParams.Params : System.Collections.Generic.IList +public readonly AdaptySDK.AdaptyOnboardingsNumberInput.Value : System.Double +public readonly AdaptySDK.AdaptyOnboardingsSelectParams.Id : System.String +public readonly AdaptySDK.AdaptyOnboardingsSelectParams.Label : System.String +public readonly AdaptySDK.AdaptyOnboardingsSelectParams.Value : System.String +public readonly AdaptySDK.AdaptyOnboardingsTextInput.Value : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.AccessLevelId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.AdaptyProductId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.FlowProductId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.IsFamilyShareable : System.Boolean +public readonly AdaptySDK.AdaptyPaywallProduct.LocalizedDescription : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.LocalizedTitle : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallABTestName : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallName : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallProductIndex : System.Int32 +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallVariationId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.Price : AdaptySDK.AdaptyPrice +public readonly AdaptySDK.AdaptyPaywallProduct.ProductType : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.RegionCode : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.Subscription : AdaptySDK.AdaptySubscription +public readonly AdaptySDK.AdaptyPaywallProduct.VendorProductId : System.String +public readonly AdaptySDK.AdaptyPlacement.ABTestName : System.String +public readonly AdaptySDK.AdaptyPlacement.AudienceName : System.String +public readonly AdaptySDK.AdaptyPlacement.Id : System.String +public readonly AdaptySDK.AdaptyPlacement.IsTrackingPurchases : System.Nullable +public readonly AdaptySDK.AdaptyPlacement.PlacementAudienceVersionId : System.String +public readonly AdaptySDK.AdaptyPlacement.Revision : System.Int64 +public readonly AdaptySDK.AdaptyPrice.Amount : System.Double +public readonly AdaptySDK.AdaptyPrice.CurrencyCode : System.String +public readonly AdaptySDK.AdaptyPrice.CurrencySymbol : System.String +public readonly AdaptySDK.AdaptyPrice.LocalizedString : System.String +public readonly AdaptySDK.AdaptyProductIdentifier.BasePlanId : System.String +public readonly AdaptySDK.AdaptyProductIdentifier.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActivatedAt : System.DateTime +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActiveIntroductoryOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActivePromotionalOfferId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActivePromotionalOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.BillingIssueDetectedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.CancellationReason : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ExpiresAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.Id : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsActive : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsInGracePeriod : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsLifetime : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsRefund : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.OfferId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.RenewedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.StartsAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.Store : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.UnsubscribedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.WillRenew : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.IsConsumable : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.IsRefund : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.IsSandbox : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.PurchaseId : System.String +public readonly AdaptySDK.AdaptyProfile+NonSubscription.PurchasedAt : System.DateTime +public readonly AdaptySDK.AdaptyProfile+NonSubscription.Store : System.String +public readonly AdaptySDK.AdaptyProfile+NonSubscription.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+NonSubscription.VendorTransactionId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ActivatedAt : System.DateTime +public readonly AdaptySDK.AdaptyProfile+Subscription.ActiveIntroductoryOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ActivePromotionalOfferId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ActivePromotionalOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.BillingIssueDetectedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.CancellationReason : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ExpiresAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.IsActive : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsInGracePeriod : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsLifetime : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsRefund : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsSandbox : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.OfferId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.RenewedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.StartsAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.Store : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.UnsubscribedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.VendorOriginalTransactionId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.VendorTransactionId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.WillRenew : System.Boolean +public readonly AdaptySDK.AdaptyProfile.CustomerUserId : System.String +public readonly AdaptySDK.AdaptyProfile.ProfileId : System.String +public readonly AdaptySDK.AdaptyPurchaseParameters.IsOfferPersonalized : System.Nullable +public readonly AdaptySDK.AdaptyPurchaseParameters.SubscriptionUpdateParams : AdaptySDK.AdaptySubscriptionUpdateParameters +public readonly AdaptySDK.AdaptyPurchaseResult.AppleJWSTransaction : System.String +public readonly AdaptySDK.AdaptyPurchaseResult.GooglePurchaseToken : System.String +public readonly AdaptySDK.AdaptyPurchaseResult.Profile : AdaptySDK.AdaptyProfile +public readonly AdaptySDK.AdaptyPurchaseResult.Type : AdaptySDK.AdaptyPurchaseResultType +public readonly AdaptySDK.AdaptyRemoteConfig.Data : System.String +public readonly AdaptySDK.AdaptyRemoteConfig.Locale : System.String +public readonly AdaptySDK.AdaptySubscription.BasePlanId : System.String +public readonly AdaptySDK.AdaptySubscription.GroupIdentifier : System.String +public readonly AdaptySDK.AdaptySubscription.LocalizedPeriod : System.String +public readonly AdaptySDK.AdaptySubscription.Offer : AdaptySDK.AdaptySubscriptionOffer +public readonly AdaptySDK.AdaptySubscription.Period : AdaptySDK.AdaptySubscriptionPeriod +public readonly AdaptySDK.AdaptySubscription.RenewalType : AdaptySDK.AdaptySubscriptionRenewalType +public readonly AdaptySDK.AdaptySubscriptionOffer.Identifier : System.String +public readonly AdaptySDK.AdaptySubscriptionOffer.OfferTags : System.Collections.Generic.IReadOnlyList +public readonly AdaptySDK.AdaptySubscriptionOffer.Phases : System.Collections.Generic.IReadOnlyList +public readonly AdaptySDK.AdaptySubscriptionOffer.Type : AdaptySDK.AdaptySubscriptionOfferType +public readonly AdaptySDK.AdaptySubscriptionPeriod.NumberOfUnits : System.Int64 +public readonly AdaptySDK.AdaptySubscriptionPeriod.Unit : AdaptySDK.AdaptySubscriptionPeriodUnit +public readonly AdaptySDK.AdaptySubscriptionPhase.LocalizedNumberOfPeriods : System.String +public readonly AdaptySDK.AdaptySubscriptionPhase.LocalizedSubscriptionPeriod : System.String +public readonly AdaptySDK.AdaptySubscriptionPhase.NumberOfPeriods : System.Int32 +public readonly AdaptySDK.AdaptySubscriptionPhase.PaymentMode : AdaptySDK.AdaptyPaymentMode +public readonly AdaptySDK.AdaptySubscriptionPhase.Price : AdaptySDK.AdaptyPrice +public readonly AdaptySDK.AdaptySubscriptionPhase.SubscriptionPeriod : AdaptySDK.AdaptySubscriptionPeriod +public readonly AdaptySDK.AdaptyUIOnboardingMeta.OnboardingId : System.String +public readonly AdaptySDK.AdaptyUIOnboardingMeta.ScreenClientId : System.String +public readonly AdaptySDK.AdaptyUIOnboardingMeta.ScreenIndex : System.Int32 +public readonly AdaptySDK.AdaptyUIOnboardingMeta.ScreensTotal : System.Int32 +public sealed class AdaptySDK.AdaptyConfiguration +public sealed class AdaptySDK.AdaptyConfiguration+Builder +public sealed class AdaptySDK.AdaptyCustomAssetColor : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLinearGradient : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalImageAsset : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalImageData : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalImageFile : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalVideoAsset : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalVideoFile : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomerIdentity +public sealed class AdaptySDK.AdaptyError +public sealed class AdaptySDK.AdaptyFlow +public sealed class AdaptySDK.AdaptyFlowPaywall +public sealed class AdaptySDK.AdaptyInstallationDetails +public sealed class AdaptySDK.AdaptyInstallationStatus +public sealed class AdaptySDK.AdaptyOnboarding +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingCompleted : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingStarted : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventProductsScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventRegistrationScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventSecondScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventUnknown : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventUserEmailCollected : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsDatePickerParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsEmailInput : AdaptySDK.AdaptyOnboardingsInput +public sealed class AdaptySDK.AdaptyOnboardingsInputParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsMultiSelectParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsNumberInput : AdaptySDK.AdaptyOnboardingsInput +public sealed class AdaptySDK.AdaptyOnboardingsSelectParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsTextInput : AdaptySDK.AdaptyOnboardingsInput +public sealed class AdaptySDK.AdaptyPaywallProduct +public sealed class AdaptySDK.AdaptyPlacement +public sealed class AdaptySDK.AdaptyPlacementFetchPolicy +public sealed class AdaptySDK.AdaptyPrice +public sealed class AdaptySDK.AdaptyProductIdentifier +public sealed class AdaptySDK.AdaptyProfile +public sealed class AdaptySDK.AdaptyProfile+AccessLevel +public sealed class AdaptySDK.AdaptyProfile+NonSubscription +public sealed class AdaptySDK.AdaptyProfile+Subscription +public sealed class AdaptySDK.AdaptyProfileParameters +public sealed class AdaptySDK.AdaptyProfileParameters+Builder +public sealed class AdaptySDK.AdaptyPurchaseParameters +public sealed class AdaptySDK.AdaptyPurchaseParametersBuilder +public sealed class AdaptySDK.AdaptyPurchaseResult +public sealed class AdaptySDK.AdaptyRemoteConfig +public sealed class AdaptySDK.AdaptySubscription +public sealed class AdaptySDK.AdaptySubscriptionOffer +public sealed class AdaptySDK.AdaptySubscriptionPeriod +public sealed class AdaptySDK.AdaptySubscriptionPhase +public sealed class AdaptySDK.AdaptySubscriptionUpdateParameters +public sealed class AdaptySDK.AdaptyUICreateFlowViewParameters +public sealed class AdaptySDK.AdaptyUIDialogConfiguration +public sealed class AdaptySDK.AdaptyUIFlowView +public sealed class AdaptySDK.AdaptyUIMediaCacheConfiguration +public sealed class AdaptySDK.AdaptyUIOnboardingMeta +public sealed class AdaptySDK.AdaptyUIOnboardingView +public sealed class AdaptySDK.AdaptyUIUserAction +public static AdaptySDK.Adapty.Activate(AdaptySDK.AdaptyConfiguration configuration, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Activate(AdaptySDK.Builder configurationBuilder, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.CreateWebPaywallUrl(AdaptySDK.AdaptyFlowPaywall paywall, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.CreateWebPaywallUrl(AdaptySDK.AdaptyPaywallProduct product, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetCurrentInstallationStatus(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlow(System.String placementId, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Nullable loadTimeout, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlow(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlowForDefaultAudience(System.String placementId, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlowForDefaultAudience(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetLogLevel(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetNativeSDKVersion(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboarding(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboarding(System.String placementId, System.String locale, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Nullable loadTimeout, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, System.String locale, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, System.String locale, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetPaywallProducts(AdaptySDK.AdaptyFlow flow, System.Action, AdaptySDK.AdaptyError> completionHandler) : System.Void +public static AdaptySDK.Adapty.GetProfile(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Identify(System.String customerUserId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Identify(System.String customerUserId, System.Guid iosAppAccountToken, System.String androidObfuscatedAccountId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.IsActivated(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.LogShowFlow(AdaptySDK.AdaptyFlow flow, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Logout(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.MakePurchase(AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyPurchaseParameters purchaseParameters, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.MakePurchase(AdaptySDK.AdaptyPaywallProduct product, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyFlowPaywall paywall, AdaptySDK.AdaptyWebPresentation openIn, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyFlowPaywall paywall, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyWebPresentation openIn, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyPaywallProduct product, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.PresentCodeRedemptionSheet(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.ReportTransaction(System.String transactionId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.ReportTransaction(System.String transactionId, System.String variationId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.RestorePurchases(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetEventListener(AdaptySDK.IAdaptyEventListener listener) : System.Void +public static AdaptySDK.Adapty.SetFallback(System.String fileName, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetFlowsEventsListener(AdaptySDK.IAdaptyFlowsEventsListener listener) : System.Void +public static AdaptySDK.Adapty.SetIntegrationIdentifier(System.String key, System.String value, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetLogLevel(AdaptySDK.AdaptyLogLevel level, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetObserverModeResolver(AdaptySDK.IAdaptyUIObserverModeResolver resolver) : System.Void +public static AdaptySDK.Adapty.SetOnboardingsEventsListener(AdaptySDK.IAdaptyOnboardingsEventsListener listener) : System.Void +public static AdaptySDK.Adapty.SetSystemRequestsHandler(AdaptySDK.IAdaptyUISystemRequestsHandler handler) : System.Void +public static AdaptySDK.Adapty.UpdateAppStoreCollectingRefundDataConsent(System.Boolean consent, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateAppStoreRefundPreference(AdaptySDK.AdaptyRefundPreference refundPreference, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateAttribution(System.Collections.Generic.IReadOnlyDictionary attribution, System.String source, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateAttribution(System.String jsonString, System.String source, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateProfile(AdaptySDK.AdaptyProfileParameters param, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyCustomAsset.Color(UnityEngine.Color color) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LinearGradient(UnityEngine.Gradient gradient) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalImageAsset(System.String assetId) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalImageData(System.Byte[] data) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalImageFile(System.String path) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalVideoAsset(System.String assetId) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalVideoFile(System.String path) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyPlacementFetchPolicy.ReturnCacheDataIfNotExpiredElseLoad(System.TimeSpan maxAge) : AdaptySDK.AdaptyPlacementFetchPolicy +public static AdaptySDK.AdaptyUI.CreateFlowView(AdaptySDK.AdaptyFlow flow, AdaptySDK.AdaptyUICreateFlowViewParameters optionalParameters, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.CreateFlowView(AdaptySDK.AdaptyFlow flow, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.CreateOnboardingView(AdaptySDK.AdaptyOnboarding onboarding, AdaptySDK.AdaptyWebPresentation externalUrlsPresentation, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.CreateOnboardingView(AdaptySDK.AdaptyOnboarding onboarding, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.DismissFlowView(AdaptySDK.AdaptyUIFlowView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.DismissOnboardingView(AdaptySDK.AdaptyUIOnboardingView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.OpenUrl(System.String url, AdaptySDK.AdaptyWebPresentation openIn, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentFlowView(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentFlowView(AdaptySDK.AdaptyUIFlowView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentOnboardingView(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentOnboardingView(AdaptySDK.AdaptyUIOnboardingView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.RequestAppReview(System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.ShowDialog(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyUIDialogConfiguration configuration, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.ShowDialog(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIDialogConfiguration configuration, System.Action completionHandler) : System.Void +public static class AdaptySDK.Adapty +public static class AdaptySDK.AdaptyUI +public static const AdaptySDK.AdaptyErrorCode.ActivateOnceError : AdaptySDK.AdaptyErrorCode = 3005 +public static const AdaptySDK.AdaptyErrorCode.AdaptyNotInitialized : AdaptySDK.AdaptyErrorCode = 20 +public static const AdaptySDK.AdaptyErrorCode.AnalyticsDisabled : AdaptySDK.AdaptyErrorCode = 3000 +public static const AdaptySDK.AdaptyErrorCode.BadRequest : AdaptySDK.AdaptyErrorCode = 2003 +public static const AdaptySDK.AdaptyErrorCode.BillingError : AdaptySDK.AdaptyErrorCode = 106 +public static const AdaptySDK.AdaptyErrorCode.BillingNetworkError : AdaptySDK.AdaptyErrorCode = 112 +public static const AdaptySDK.AdaptyErrorCode.BillingServiceDisconnected : AdaptySDK.AdaptyErrorCode = 99 +public static const AdaptySDK.AdaptyErrorCode.BillingServiceTimeout : AdaptySDK.AdaptyErrorCode = 97 +public static const AdaptySDK.AdaptyErrorCode.BillingServiceUnavailable : AdaptySDK.AdaptyErrorCode = 102 +public static const AdaptySDK.AdaptyErrorCode.BillingUnavailable : AdaptySDK.AdaptyErrorCode = 103 +public static const AdaptySDK.AdaptyErrorCode.CantMakePayments : AdaptySDK.AdaptyErrorCode = 1003 +public static const AdaptySDK.AdaptyErrorCode.CantReadReceipt : AdaptySDK.AdaptyErrorCode = 1005 +public static const AdaptySDK.AdaptyErrorCode.ClientInvalid : AdaptySDK.AdaptyErrorCode = 1 +public static const AdaptySDK.AdaptyErrorCode.CloudServiceNetworkConnectionFailed : AdaptySDK.AdaptyErrorCode = 7 +public static const AdaptySDK.AdaptyErrorCode.CloudServicePermissionDenied : AdaptySDK.AdaptyErrorCode = 6 +public static const AdaptySDK.AdaptyErrorCode.CloudServiceRevoked : AdaptySDK.AdaptyErrorCode = 8 +public static const AdaptySDK.AdaptyErrorCode.CurrentSubscriptionToUpdateNotFoundInHistory : AdaptySDK.AdaptyErrorCode = 24 +public static const AdaptySDK.AdaptyErrorCode.DecodingFailed : AdaptySDK.AdaptyErrorCode = 2006 +public static const AdaptySDK.AdaptyErrorCode.DeveloperError : AdaptySDK.AdaptyErrorCode = 105 +public static const AdaptySDK.AdaptyErrorCode.EncodingFailed : AdaptySDK.AdaptyErrorCode = 2009 +public static const AdaptySDK.AdaptyErrorCode.FeatureNotSupported : AdaptySDK.AdaptyErrorCode = 98 +public static const AdaptySDK.AdaptyErrorCode.FetchSubscriptionStatusFailed : AdaptySDK.AdaptyErrorCode = 1020 +public static const AdaptySDK.AdaptyErrorCode.FetchTimeoutError : AdaptySDK.AdaptyErrorCode = 3101 +public static const AdaptySDK.AdaptyErrorCode.InvalidActionUrl : AdaptySDK.AdaptyErrorCode = 4107 +public static const AdaptySDK.AdaptyErrorCode.InvalidOfferIdentifier : AdaptySDK.AdaptyErrorCode = 11 +public static const AdaptySDK.AdaptyErrorCode.InvalidOfferPrice : AdaptySDK.AdaptyErrorCode = 14 +public static const AdaptySDK.AdaptyErrorCode.InvalidSignature : AdaptySDK.AdaptyErrorCode = 12 +public static const AdaptySDK.AdaptyErrorCode.ItemAlreadyOwned : AdaptySDK.AdaptyErrorCode = 107 +public static const AdaptySDK.AdaptyErrorCode.ItemNotOwned : AdaptySDK.AdaptyErrorCode = 108 +public static const AdaptySDK.AdaptyErrorCode.JsException : AdaptySDK.AdaptyErrorCode = 4105 +public static const AdaptySDK.AdaptyErrorCode.MissingOfferParams : AdaptySDK.AdaptyErrorCode = 13 +public static const AdaptySDK.AdaptyErrorCode.NavigatorNotFound : AdaptySDK.AdaptyErrorCode = 4106 +public static const AdaptySDK.AdaptyErrorCode.NetworkFailed : AdaptySDK.AdaptyErrorCode = 2005 +public static const AdaptySDK.AdaptyErrorCode.NoProductIDsFound : AdaptySDK.AdaptyErrorCode = 1000 +public static const AdaptySDK.AdaptyErrorCode.NoPurchasesToRestore : AdaptySDK.AdaptyErrorCode = 1004 +public static const AdaptySDK.AdaptyErrorCode.NotActivated : AdaptySDK.AdaptyErrorCode = 2002 +public static const AdaptySDK.AdaptyErrorCode.OperationInterrupted : AdaptySDK.AdaptyErrorCode = 9000 +public static const AdaptySDK.AdaptyErrorCode.PaymentCancelled : AdaptySDK.AdaptyErrorCode = 2 +public static const AdaptySDK.AdaptyErrorCode.PaymentInvalid : AdaptySDK.AdaptyErrorCode = 3 +public static const AdaptySDK.AdaptyErrorCode.PaymentNotAllowed : AdaptySDK.AdaptyErrorCode = 4 +public static const AdaptySDK.AdaptyErrorCode.PaymentPendingError : AdaptySDK.AdaptyErrorCode = 1050 +public static const AdaptySDK.AdaptyErrorCode.PrivacyAcknowledgementRequired : AdaptySDK.AdaptyErrorCode = 9 +public static const AdaptySDK.AdaptyErrorCode.ProductNotFound : AdaptySDK.AdaptyErrorCode = 22 +public static const AdaptySDK.AdaptyErrorCode.ProductPurchaseFailed : AdaptySDK.AdaptyErrorCode = 1006 +public static const AdaptySDK.AdaptyErrorCode.ProductRequestFailed : AdaptySDK.AdaptyErrorCode = 1002 +public static const AdaptySDK.AdaptyErrorCode.ProfileWasChanged : AdaptySDK.AdaptyErrorCode = 3006 +public static const AdaptySDK.AdaptyErrorCode.RefreshReceiptFailed : AdaptySDK.AdaptyErrorCode = 1010 +public static const AdaptySDK.AdaptyErrorCode.ServerError : AdaptySDK.AdaptyErrorCode = 2004 +public static const AdaptySDK.AdaptyErrorCode.StoreProductNotAvailable : AdaptySDK.AdaptyErrorCode = 5 +public static const AdaptySDK.AdaptyErrorCode.UnauthorizedRequestData : AdaptySDK.AdaptyErrorCode = 10 +public static const AdaptySDK.AdaptyErrorCode.UnidentifiedUserLogout : AdaptySDK.AdaptyErrorCode = 3020 +public static const AdaptySDK.AdaptyErrorCode.Unknown : AdaptySDK.AdaptyErrorCode = 0 +public static const AdaptySDK.AdaptyErrorCode.UnsupportedData : AdaptySDK.AdaptyErrorCode = 3007 +public static const AdaptySDK.AdaptyErrorCode.WrongAssetType : AdaptySDK.AdaptyErrorCode = 4104 +public static const AdaptySDK.AdaptyErrorCode.WrongParam : AdaptySDK.AdaptyErrorCode = 3001 +public static const AdaptySDK.AdaptyInstallationStatusType.Determined : AdaptySDK.AdaptyInstallationStatusType = 2 +public static const AdaptySDK.AdaptyInstallationStatusType.NotAvailable : AdaptySDK.AdaptyInstallationStatusType = 0 +public static const AdaptySDK.AdaptyInstallationStatusType.NotDetermined : AdaptySDK.AdaptyInstallationStatusType = 1 +public static const AdaptySDK.AdaptyLogLevel.Debug : AdaptySDK.AdaptyLogLevel = 4 +public static const AdaptySDK.AdaptyLogLevel.Error : AdaptySDK.AdaptyLogLevel = 0 +public static const AdaptySDK.AdaptyLogLevel.Info : AdaptySDK.AdaptyLogLevel = 2 +public static const AdaptySDK.AdaptyLogLevel.Verbose : AdaptySDK.AdaptyLogLevel = 3 +public static const AdaptySDK.AdaptyLogLevel.Warn : AdaptySDK.AdaptyLogLevel = 1 +public static const AdaptySDK.AdaptyPaymentMode.FreeTrial : AdaptySDK.AdaptyPaymentMode = 2 +public static const AdaptySDK.AdaptyPaymentMode.PayAsYouGo : AdaptySDK.AdaptyPaymentMode = 0 +public static const AdaptySDK.AdaptyPaymentMode.PayUpFront : AdaptySDK.AdaptyPaymentMode = 1 +public static const AdaptySDK.AdaptyPaymentMode.Unknown : AdaptySDK.AdaptyPaymentMode = 3 +public static const AdaptySDK.AdaptyProfileGender.Female : AdaptySDK.AdaptyProfileGender = 0 +public static const AdaptySDK.AdaptyProfileGender.Male : AdaptySDK.AdaptyProfileGender = 1 +public static const AdaptySDK.AdaptyProfileGender.Other : AdaptySDK.AdaptyProfileGender = 2 +public static const AdaptySDK.AdaptyPurchaseResultType.Pending : AdaptySDK.AdaptyPurchaseResultType = 0 +public static const AdaptySDK.AdaptyPurchaseResultType.Success : AdaptySDK.AdaptyPurchaseResultType = 2 +public static const AdaptySDK.AdaptyPurchaseResultType.UserCancelled : AdaptySDK.AdaptyPurchaseResultType = 1 +public static const AdaptySDK.AdaptyRefundPreference.Decline : AdaptySDK.AdaptyRefundPreference = 2 +public static const AdaptySDK.AdaptyRefundPreference.Grant : AdaptySDK.AdaptyRefundPreference = 1 +public static const AdaptySDK.AdaptyRefundPreference.NoPreference : AdaptySDK.AdaptyRefundPreference = 0 +public static const AdaptySDK.AdaptyServerCluster.CN : AdaptySDK.AdaptyServerCluster = 2 +public static const AdaptySDK.AdaptyServerCluster.Default : AdaptySDK.AdaptyServerCluster = 0 +public static const AdaptySDK.AdaptyServerCluster.EU : AdaptySDK.AdaptyServerCluster = 1 +public static const AdaptySDK.AdaptySubscriptionOfferType.Code : AdaptySDK.AdaptySubscriptionOfferType = 3 +public static const AdaptySDK.AdaptySubscriptionOfferType.Introductory : AdaptySDK.AdaptySubscriptionOfferType = 0 +public static const AdaptySDK.AdaptySubscriptionOfferType.Promotional : AdaptySDK.AdaptySubscriptionOfferType = 1 +public static const AdaptySDK.AdaptySubscriptionOfferType.WinBack : AdaptySDK.AdaptySubscriptionOfferType = 2 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Day : AdaptySDK.AdaptySubscriptionPeriodUnit = 0 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Month : AdaptySDK.AdaptySubscriptionPeriodUnit = 2 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Unknown : AdaptySDK.AdaptySubscriptionPeriodUnit = 4 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Week : AdaptySDK.AdaptySubscriptionPeriodUnit = 1 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Year : AdaptySDK.AdaptySubscriptionPeriodUnit = 3 +public static const AdaptySDK.AdaptySubscriptionRenewalType.Autorenewable : AdaptySDK.AdaptySubscriptionRenewalType = 1 +public static const AdaptySDK.AdaptySubscriptionRenewalType.Prepaid : AdaptySDK.AdaptySubscriptionRenewalType = 0 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.ChargeFullPrice : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 4 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.ChargeProratedPrice : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 1 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.Deferred : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 3 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.WithTimeProration : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 0 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.WithoutProration : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 2 +public static const AdaptySDK.AdaptyUIDialogActionType.Primary : AdaptySDK.AdaptyUIDialogActionType = 0 +public static const AdaptySDK.AdaptyUIDialogActionType.Secondary : AdaptySDK.AdaptyUIDialogActionType = 1 +public static const AdaptySDK.AdaptyUIIOSPresentationStyle.FullScreen : AdaptySDK.AdaptyUIIOSPresentationStyle = 0 +public static const AdaptySDK.AdaptyUIIOSPresentationStyle.PageSheet : AdaptySDK.AdaptyUIIOSPresentationStyle = 1 +public static const AdaptySDK.AdaptyUIUserActionType.Close : AdaptySDK.AdaptyUIUserActionType = 0 +public static const AdaptySDK.AdaptyUIUserActionType.Custom : AdaptySDK.AdaptyUIUserActionType = 3 +public static const AdaptySDK.AdaptyUIUserActionType.OpenUrl : AdaptySDK.AdaptyUIUserActionType = 2 +public static const AdaptySDK.AdaptyUIUserActionType.SystemBack : AdaptySDK.AdaptyUIUserActionType = 1 +public static const AdaptySDK.AdaptyWebPresentation.ExternalBrowser : AdaptySDK.AdaptyWebPresentation = 0 +public static const AdaptySDK.AdaptyWebPresentation.InAppBrowser : AdaptySDK.AdaptyWebPresentation = 1 +public static const AdaptySDK.AppTrackingTransparencyStatus.Authorized : AdaptySDK.AppTrackingTransparencyStatus = 3 +public static const AdaptySDK.AppTrackingTransparencyStatus.Denied : AdaptySDK.AppTrackingTransparencyStatus = 2 +public static const AdaptySDK.AppTrackingTransparencyStatus.NotDetermined : AdaptySDK.AppTrackingTransparencyStatus = 0 +public static const AdaptySDK.AppTrackingTransparencyStatus.Restricted : AdaptySDK.AppTrackingTransparencyStatus = 1 +public static readonly AdaptySDK.Adapty.SDKVersion : System.String +public static readonly AdaptySDK.AdaptyPlacementFetchPolicy.Default : AdaptySDK.AdaptyPlacementFetchPolicy +public static readonly AdaptySDK.AdaptyPlacementFetchPolicy.ReloadRevalidatingCacheData : AdaptySDK.AdaptyPlacementFetchPolicy +public static readonly AdaptySDK.AdaptyPlacementFetchPolicy.ReturnCacheDataElseLoad : AdaptySDK.AdaptyPlacementFetchPolicy \ No newline at end of file diff --git a/tests/shared/Fixtures/approved/public-surface.ios.approved.txt b/tests/shared/Fixtures/approved/public-surface.ios.approved.txt new file mode 100644 index 0000000..d2e1cb7 --- /dev/null +++ b/tests/shared/Fixtures/approved/public-surface.ios.approved.txt @@ -0,0 +1,653 @@ +protected AdaptySDK.AdaptyCustomAsset.ctor() +protected AdaptySDK.AdaptyOnboardingsAnalyticsEvent.ctor() +protected AdaptySDK.AdaptyOnboardingsInput.ctor() +protected AdaptySDK.AdaptyOnboardingsStateUpdatedParams.ctor() +public AdaptySDK.AdaptyConfiguration+Builder.ActivateUI : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.AdaptyUIMediaCache : AdaptySDK.AdaptyUIMediaCacheConfiguration +public AdaptySDK.AdaptyConfiguration+Builder.ApiKey : System.String +public AdaptySDK.AdaptyConfiguration+Builder.AppleClearDataOnBackup : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.AppleIdfaCollectionDisabled : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.BackendProxyHost : System.String +public AdaptySDK.AdaptyConfiguration+Builder.BackendProxyPort : System.Int32 +public AdaptySDK.AdaptyConfiguration+Builder.Build() : AdaptySDK.AdaptyConfiguration +public AdaptySDK.AdaptyConfiguration+Builder.CustomerIdentity : AdaptySDK.AdaptyCustomerIdentity +public AdaptySDK.AdaptyConfiguration+Builder.CustomerUserId : System.String +public AdaptySDK.AdaptyConfiguration+Builder.GoogleAdvertisingIdCollectionDisabled : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.GoogleEnablePendingPrepaidPlans : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.GoogleLocalAccessLevelAllowed : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.IpAddressCollectionDisabled : System.Boolean +public AdaptySDK.AdaptyConfiguration+Builder.LogLevel : AdaptySDK.AdaptyLogLevel +public AdaptySDK.AdaptyConfiguration+Builder.ObserverMode : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.ServerCluster : System.Nullable +public AdaptySDK.AdaptyConfiguration+Builder.SetAPIKey(System.String apiKey) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetActivateUI(System.Boolean activate) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetAdaptyUIMediaCache(System.Nullable memoryStorageTotalCostLimit, System.Nullable memoryStorageCountLimit, System.Nullable diskStorageSizeLimit) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetAppleClearDataOnBackup(System.Boolean appleClearDataOnBackup) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetAppleIDFACollectionDisabled(System.Boolean appleIdfaCollectionDisabled) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetBackendProxy(System.String host, System.Int32 port) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetCustomerUserId(System.String customerUserId) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetCustomerUserId(System.String customerUserId, System.Guid iosAppAccountToken, System.String androidObfuscatedAccountId) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetGoogleAdvertisingIdCollectionDisabled(System.Boolean googleAdvertisingIdCollectionDisabled) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetGoogleEnablePendingPrepaidPlans(System.Boolean googleEnablePendingPrepaidPlans) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetGoogleLocalAccessLevelAllowed(System.Boolean googleLocalAccessLevelAllowed) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetIPAddressCollectionDisabled(System.Boolean ipAddressCollectionDisabled) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetObserverMode(System.Boolean observerMode) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.SetServerCluster(AdaptySDK.AdaptyServerCluster serverCluster) : AdaptySDK.Builder +public AdaptySDK.AdaptyConfiguration+Builder.ctor(System.String apiKey) +public AdaptySDK.AdaptyCustomAssetColor.ColorValue : UnityEngine.Color { get; } +public AdaptySDK.AdaptyCustomAssetLinearGradient.Gradient : UnityEngine.Gradient { get; } +public AdaptySDK.AdaptyCustomAssetLocalImageAsset.AssetId : System.String { get; } +public AdaptySDK.AdaptyCustomAssetLocalImageData.Data : System.Byte[] { get; } +public AdaptySDK.AdaptyCustomAssetLocalImageFile.Path : System.String { get; } +public AdaptySDK.AdaptyCustomAssetLocalVideoAsset.AssetId : System.String { get; } +public AdaptySDK.AdaptyCustomAssetLocalVideoFile.Path : System.String { get; } +public AdaptySDK.AdaptyCustomerIdentity.IsEmpty : System.Boolean { get; } +public AdaptySDK.AdaptyCustomerIdentity.ctor(System.Guid iosAppAccountToken, System.String androidObfuscatedAccountId) +public AdaptySDK.AdaptyErrorCode.value__ : System.Int32 +public AdaptySDK.AdaptyFlow.Paywalls : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlow.ProductIdentifiers : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlow.RemoteConfig : AdaptySDK.AdaptyRemoteConfig { get; } +public AdaptySDK.AdaptyFlow.RemoteConfigs : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlow.VendorProductIds : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlowPaywall.ProductIdentifiers : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyFlowPaywall.VendorProductIds : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyInstallationStatus.Details : AdaptySDK.AdaptyInstallationDetails { get; } +public AdaptySDK.AdaptyInstallationStatusType.value__ : System.Int32 +public AdaptySDK.AdaptyLogLevel.value__ : System.Int32 +public AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingCompleted.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingStarted.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventProductsScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventRegistrationScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted.ctor(System.String elementId, System.String reply) +public AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventSecondScreenPresented.ctor() +public AdaptySDK.AdaptyOnboardingsAnalyticsEventUnknown.ctor(System.String name) +public AdaptySDK.AdaptyOnboardingsAnalyticsEventUserEmailCollected.ctor() +public AdaptySDK.AdaptyOnboardingsDatePickerParams.ctor(System.Nullable day, System.Nullable month, System.Nullable year) +public AdaptySDK.AdaptyOnboardingsEmailInput.ctor(System.String value) +public AdaptySDK.AdaptyOnboardingsInputParams.ctor(AdaptySDK.AdaptyOnboardingsInput input) +public AdaptySDK.AdaptyOnboardingsMultiSelectParams.ctor(System.Collections.Generic.IList params) +public AdaptySDK.AdaptyOnboardingsNumberInput.ctor(System.Double value) +public AdaptySDK.AdaptyOnboardingsSelectParams.ctor(System.String id, System.String value, System.String label) +public AdaptySDK.AdaptyOnboardingsTextInput.ctor(System.String value) +public AdaptySDK.AdaptyPaymentMode.value__ : System.Int32 +public AdaptySDK.AdaptyProductIdentifier.ctor(System.String vendorProductId, System.String adaptyProductId, System.String basePlanId) +public AdaptySDK.AdaptyProfile.AccessLevels : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfile.AppliedAttributionSources : System.Collections.Generic.IReadOnlyList { get; } +public AdaptySDK.AdaptyProfile.CustomAttributes : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfile.NonSubscriptions : System.Collections.Generic.IReadOnlyDictionary> { get; } +public AdaptySDK.AdaptyProfile.Subscriptions : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfileGender.value__ : System.Int32 +public AdaptySDK.AdaptyProfileParameters+Builder.Build() : AdaptySDK.AdaptyProfileParameters +public AdaptySDK.AdaptyProfileParameters+Builder.RemoveCustomAttribute(System.String key) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetAnalyticsDisabled(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetAppTrackingTransparencyStatus(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetBirthday(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetCustomDoubleAttribute(System.String key, System.Double value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetCustomStringAttribute(System.String key, System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetEmail(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetFirstName(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetGender(System.Nullable value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetLastName(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.SetPhoneNumber(System.String value) : AdaptySDK.Builder +public AdaptySDK.AdaptyProfileParameters+Builder.ctor() +public AdaptySDK.AdaptyProfileParameters.AnalyticsDisabled : System.Nullable +public AdaptySDK.AdaptyProfileParameters.AppTrackingTransparencyStatus : System.Nullable +public AdaptySDK.AdaptyProfileParameters.Birthday : System.Nullable +public AdaptySDK.AdaptyProfileParameters.CustomAttributes : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyProfileParameters.Email : System.String +public AdaptySDK.AdaptyProfileParameters.FirstName : System.String +public AdaptySDK.AdaptyProfileParameters.Gender : System.Nullable +public AdaptySDK.AdaptyProfileParameters.LastName : System.String +public AdaptySDK.AdaptyProfileParameters.PhoneNumber : System.String +public AdaptySDK.AdaptyProfileParameters.RemoveCustomAttribute(System.String key) : System.Void +public AdaptySDK.AdaptyProfileParameters.SetCustomDoubleAttribute(System.String key, System.Double value) : System.Void +public AdaptySDK.AdaptyProfileParameters.SetCustomStringAttribute(System.String key, System.String value) : System.Void +public AdaptySDK.AdaptyProfileParameters.ctor() +public AdaptySDK.AdaptyPurchaseParameters.ctor(AdaptySDK.AdaptySubscriptionUpdateParameters subscriptionUpdateParams = null, System.Nullable isOfferPersonalized = null) +public AdaptySDK.AdaptyPurchaseParametersBuilder.Build() : AdaptySDK.AdaptyPurchaseParameters +public AdaptySDK.AdaptyPurchaseParametersBuilder.SetIsOfferPersonalized(System.Nullable isOfferPersonalized) : AdaptySDK.AdaptyPurchaseParametersBuilder +public AdaptySDK.AdaptyPurchaseParametersBuilder.SetSubscriptionUpdateParams(AdaptySDK.AdaptySubscriptionUpdateParameters subscriptionUpdateParams) : AdaptySDK.AdaptyPurchaseParametersBuilder +public AdaptySDK.AdaptyPurchaseParametersBuilder.ctor() +public AdaptySDK.AdaptyPurchaseResultType.value__ : System.Int32 +public AdaptySDK.AdaptyRefundPreference.value__ : System.Int32 +public AdaptySDK.AdaptyRemoteConfig.Dictionary : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyServerCluster.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionOfferType.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionPeriodUnit.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionRenewalType.value__ : System.Int32 +public AdaptySDK.AdaptySubscriptionUpdateParameters.OldSubVendorProductId : System.String +public AdaptySDK.AdaptySubscriptionUpdateParameters.ReplacementMode : AdaptySDK.AdaptySubscriptionUpdateReplacementMode +public AdaptySDK.AdaptySubscriptionUpdateParameters.ctor(System.String oldSubVendorProductId, AdaptySDK.AdaptySubscriptionUpdateReplacementMode replacementMode) +public AdaptySDK.AdaptySubscriptionUpdateReplacementMode.value__ : System.Int32 +public AdaptySDK.AdaptyUICreateFlowViewParameters.CustomAssets : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.CustomTags : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.CustomTimers : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.EnableSafeAreaPaddings : System.Nullable +public AdaptySDK.AdaptyUICreateFlowViewParameters.LoadTimeout : System.Nullable +public AdaptySDK.AdaptyUICreateFlowViewParameters.Locale : System.String +public AdaptySDK.AdaptyUICreateFlowViewParameters.PreloadProducts : System.Nullable +public AdaptySDK.AdaptyUICreateFlowViewParameters.ProductPurchaseParameters : System.Collections.Generic.IReadOnlyDictionary { get; } +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetCustomAssets(System.Collections.Generic.IReadOnlyDictionary customAssets) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetCustomTags(System.Collections.Generic.IReadOnlyDictionary customTags) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetCustomTimers(System.Collections.Generic.IReadOnlyDictionary customTimers) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetEnableSafeAreaPaddings(System.Nullable enableSafeAreaPaddings) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetLoadTimeout(System.Nullable loadTimeout) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetLocale(System.String locale) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetPreloadProducts(System.Nullable preloadProducts) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.SetProductPurchaseParameters(System.Collections.Generic.IReadOnlyDictionary productPurchaseParameters) : AdaptySDK.AdaptyUICreateFlowViewParameters +public AdaptySDK.AdaptyUICreateFlowViewParameters.ctor() +public AdaptySDK.AdaptyUIDialogActionType.value__ : System.Int32 +public AdaptySDK.AdaptyUIDialogConfiguration.Content : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.DefaultActionTitle : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.SecondaryActionTitle : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.SetContent(System.String content) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.SetDefaultActionTitle(System.String defaultActionTitle) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.SetSecondaryActionTitle(System.String secondaryActionTitle) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.SetTitle(System.String title) : AdaptySDK.AdaptyUIDialogConfiguration +public AdaptySDK.AdaptyUIDialogConfiguration.Title : System.String +public AdaptySDK.AdaptyUIDialogConfiguration.ctor() +public AdaptySDK.AdaptyUIFlowView.Dismiss(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIFlowView.Id : System.String +public AdaptySDK.AdaptyUIFlowView.Locale : System.String +public AdaptySDK.AdaptyUIFlowView.PlacementId : System.String +public AdaptySDK.AdaptyUIFlowView.Present(AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIFlowView.Present(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIFlowView.VariationId : System.String +public AdaptySDK.AdaptyUIIOSPresentationStyle.value__ : System.Int32 +public AdaptySDK.AdaptyUIMediaCacheConfiguration.DiskStorageSizeLimit : System.Nullable +public AdaptySDK.AdaptyUIMediaCacheConfiguration.MemoryStorageCountLimit : System.Nullable +public AdaptySDK.AdaptyUIMediaCacheConfiguration.MemoryStorageTotalCostLimit : System.Nullable +public AdaptySDK.AdaptyUIMediaCacheConfiguration.ctor(System.Nullable memoryStorageTotalCostLimit, System.Nullable memoryStorageCountLimit, System.Nullable diskStorageSizeLimit) +public AdaptySDK.AdaptyUIOnboardingView.Dismiss(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIOnboardingView.Id : System.String +public AdaptySDK.AdaptyUIOnboardingView.PaywallVariationId : System.String +public AdaptySDK.AdaptyUIOnboardingView.PlacementId : System.String +public AdaptySDK.AdaptyUIOnboardingView.Present(AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIOnboardingView.Present(System.Action completionHandler) : System.Void +public AdaptySDK.AdaptyUIUserAction.OpenIn : System.Nullable +public AdaptySDK.AdaptyUIUserAction.Type : AdaptySDK.AdaptyUIUserActionType +public AdaptySDK.AdaptyUIUserAction.Value : System.String +public AdaptySDK.AdaptyUIUserActionType.value__ : System.Int32 +public AdaptySDK.AdaptyWebPresentation.value__ : System.Int32 +public AdaptySDK.AppTrackingTransparencyStatus.value__ : System.Int32 +public abstract AdaptySDK.IAdaptyEventListener.OnInstallationDetailsFail(AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyEventListener.OnInstallationDetailsSuccess(AdaptySDK.AdaptyInstallationDetails details) : System.Void +public abstract AdaptySDK.IAdaptyEventListener.OnLoadLatestProfile(AdaptySDK.AdaptyProfile profile) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidAppear(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidDisappear(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFailLoadingProducts(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFailPurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFailRestore(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFinishPurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyPurchaseResult purchasedResult) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFinishRestore(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyProfile profile) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidFinishWebPaymentNavigation(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidPerformAction(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyUIUserAction action) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidReceiveAnalyticEvent(AdaptySDK.AdaptyUIFlowView view, System.String name, System.Collections.Generic.IReadOnlyDictionary parameters) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidReceiveError(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidSelectProduct(AdaptySDK.AdaptyUIFlowView view, System.String productId) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidStartPurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product) : System.Void +public abstract AdaptySDK.IAdaptyFlowsEventsListener.FlowViewDidStartRestore(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewDidFailWithError(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyError error) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewDidFinishLoading(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnAnalyticsEvent(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, AdaptySDK.AdaptyOnboardingsAnalyticsEvent analyticsEvent) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnCloseAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String actionId) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnCustomAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String actionId) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnPaywallAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String actionId) : System.Void +public abstract AdaptySDK.IAdaptyOnboardingsEventsListener.OnboardingViewOnStateUpdatedAction(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIOnboardingMeta meta, System.String elementId, AdaptySDK.AdaptyOnboardingsStateUpdatedParams params) : System.Void +public abstract AdaptySDK.IAdaptyUIObserverModeResolver.FlowViewDidInitiatePurchase(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyPaywallProduct product, System.Action onStartPurchase, System.Action onFinishPurchase) : System.Void +public abstract AdaptySDK.IAdaptyUIObserverModeResolver.FlowViewDidInitiateRestore(AdaptySDK.AdaptyUIFlowView view, System.Action onStartRestore, System.Action onFinishRestore) : System.Void +public abstract AdaptySDK.IAdaptyUISystemRequestsHandler.FlowViewDidAskPermission(AdaptySDK.AdaptyUIFlowView view, System.String permission, System.Collections.Generic.IReadOnlyDictionary customArgs, System.Action respond) : System.Void +public abstract AdaptySDK.IAdaptyUISystemRequestsHandler.FlowViewDidRequestAppReview(AdaptySDK.AdaptyUIFlowView view) : System.Void +public abstract class AdaptySDK.AdaptyCustomAsset +public abstract class AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public abstract class AdaptySDK.AdaptyOnboardingsInput +public abstract class AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public enum AdaptySDK.AdaptyErrorCode : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyInstallationStatusType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyLogLevel : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyPaymentMode : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyProfileGender : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyPurchaseResultType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyRefundPreference : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyServerCluster : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionOfferType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionPeriodUnit : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionRenewalType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptySubscriptionUpdateReplacementMode : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyUIDialogActionType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyUIIOSPresentationStyle : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyUIUserActionType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AdaptyWebPresentation : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public enum AdaptySDK.AppTrackingTransparencyStatus : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable +public interface AdaptySDK.IAdaptyEventListener +public interface AdaptySDK.IAdaptyFlowsEventsListener +public interface AdaptySDK.IAdaptyOnboardingsEventsListener +public interface AdaptySDK.IAdaptyUIObserverModeResolver +public interface AdaptySDK.IAdaptyUISystemRequestsHandler +public override AdaptySDK.AdaptyConfiguration+Builder.ToString() : System.String +public override AdaptySDK.AdaptyConfiguration.ToString() : System.String +public override AdaptySDK.AdaptyCustomerIdentity.ToString() : System.String +public override AdaptySDK.AdaptyError.ToString() : System.String +public override AdaptySDK.AdaptyFlow.ToString() : System.String +public override AdaptySDK.AdaptyFlowPaywall.ToString() : System.String +public override AdaptySDK.AdaptyInstallationDetails.ToString() : System.String +public override AdaptySDK.AdaptyInstallationStatus.ToString() : System.String +public override AdaptySDK.AdaptyOnboarding.ToString() : System.String +public override AdaptySDK.AdaptyOnboardingsDatePickerParams.ToString() : System.String +public override AdaptySDK.AdaptyOnboardingsMultiSelectParams.ToString() : System.String +public override AdaptySDK.AdaptyOnboardingsSelectParams.ToString() : System.String +public override AdaptySDK.AdaptyPaywallProduct.ToString() : System.String +public override AdaptySDK.AdaptyPlacement.ToString() : System.String +public override AdaptySDK.AdaptyPlacementFetchPolicy.ToString() : System.String +public override AdaptySDK.AdaptyPrice.ToString() : System.String +public override AdaptySDK.AdaptyProductIdentifier.Equals(System.Object obj) : System.Boolean +public override AdaptySDK.AdaptyProductIdentifier.GetHashCode() : System.Int32 +public override AdaptySDK.AdaptyProductIdentifier.ToString() : System.String +public override AdaptySDK.AdaptyProfile+AccessLevel.ToString() : System.String +public override AdaptySDK.AdaptyProfile+NonSubscription.ToString() : System.String +public override AdaptySDK.AdaptyProfile+Subscription.ToString() : System.String +public override AdaptySDK.AdaptyProfile.ToString() : System.String +public override AdaptySDK.AdaptyProfileParameters.ToString() : System.String +public override AdaptySDK.AdaptyPurchaseParameters.ToString() : System.String +public override AdaptySDK.AdaptyPurchaseResult.ToString() : System.String +public override AdaptySDK.AdaptySubscription.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionOffer.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionPeriod.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionPhase.ToString() : System.String +public override AdaptySDK.AdaptySubscriptionUpdateParameters.ToString() : System.String +public override AdaptySDK.AdaptyUICreateFlowViewParameters.ToString() : System.String +public override AdaptySDK.AdaptyUIDialogConfiguration.ToString() : System.String +public override AdaptySDK.AdaptyUIFlowView.ToString() : System.String +public override AdaptySDK.AdaptyUIMediaCacheConfiguration.ToString() : System.String +public override AdaptySDK.AdaptyUIOnboardingMeta.ToString() : System.String +public override AdaptySDK.AdaptyUIOnboardingView.ToString() : System.String +public override AdaptySDK.AdaptyUIUserAction.ToString() : System.String +public readonly AdaptySDK.AdaptyCustomerIdentity.AndroidObfuscatedAccountId : System.String +public readonly AdaptySDK.AdaptyCustomerIdentity.IosAppAccountToken : System.Guid +public readonly AdaptySDK.AdaptyError.Code : AdaptySDK.AdaptyErrorCode +public readonly AdaptySDK.AdaptyError.Detail : System.String +public readonly AdaptySDK.AdaptyError.Message : System.String +public readonly AdaptySDK.AdaptyFlow.FlowVersionId : System.String +public readonly AdaptySDK.AdaptyFlow.InstanceIdentity : System.String +public readonly AdaptySDK.AdaptyFlow.Name : System.String +public readonly AdaptySDK.AdaptyFlow.Placement : AdaptySDK.AdaptyPlacement +public readonly AdaptySDK.AdaptyFlow.VariationId : System.String +public readonly AdaptySDK.AdaptyFlowPaywall.InstanceIdentity : System.String +public readonly AdaptySDK.AdaptyFlowPaywall.Name : System.String +public readonly AdaptySDK.AdaptyFlowPaywall.Placement : AdaptySDK.AdaptyPlacement +public readonly AdaptySDK.AdaptyFlowPaywall.VariationId : System.String +public readonly AdaptySDK.AdaptyInstallationDetails.AppLaunchCount : System.Int32 +public readonly AdaptySDK.AdaptyInstallationDetails.InstallId : System.String +public readonly AdaptySDK.AdaptyInstallationDetails.InstallTime : System.DateTime +public readonly AdaptySDK.AdaptyInstallationDetails.Payload : System.String +public readonly AdaptySDK.AdaptyInstallationStatus.Status : AdaptySDK.AdaptyInstallationStatusType +public readonly AdaptySDK.AdaptyOnboarding.Name : System.String +public readonly AdaptySDK.AdaptyOnboarding.OnboardingId : System.String +public readonly AdaptySDK.AdaptyOnboarding.Placement : AdaptySDK.AdaptyPlacement +public readonly AdaptySDK.AdaptyOnboarding.RemoteConfig : AdaptySDK.AdaptyRemoteConfig +public readonly AdaptySDK.AdaptyOnboarding.VariationId : System.String +public readonly AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted.ElementId : System.String +public readonly AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted.Reply : System.String +public readonly AdaptySDK.AdaptyOnboardingsAnalyticsEventUnknown.Name : System.String +public readonly AdaptySDK.AdaptyOnboardingsDatePickerParams.Day : System.Nullable +public readonly AdaptySDK.AdaptyOnboardingsDatePickerParams.Month : System.Nullable +public readonly AdaptySDK.AdaptyOnboardingsDatePickerParams.Year : System.Nullable +public readonly AdaptySDK.AdaptyOnboardingsEmailInput.Value : System.String +public readonly AdaptySDK.AdaptyOnboardingsInputParams.Input : AdaptySDK.AdaptyOnboardingsInput +public readonly AdaptySDK.AdaptyOnboardingsMultiSelectParams.Params : System.Collections.Generic.IList +public readonly AdaptySDK.AdaptyOnboardingsNumberInput.Value : System.Double +public readonly AdaptySDK.AdaptyOnboardingsSelectParams.Id : System.String +public readonly AdaptySDK.AdaptyOnboardingsSelectParams.Label : System.String +public readonly AdaptySDK.AdaptyOnboardingsSelectParams.Value : System.String +public readonly AdaptySDK.AdaptyOnboardingsTextInput.Value : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.AccessLevelId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.AdaptyProductId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.FlowProductId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.IsFamilyShareable : System.Boolean +public readonly AdaptySDK.AdaptyPaywallProduct.LocalizedDescription : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.LocalizedTitle : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallABTestName : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallName : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallProductIndex : System.Int32 +public readonly AdaptySDK.AdaptyPaywallProduct.PaywallVariationId : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.Price : AdaptySDK.AdaptyPrice +public readonly AdaptySDK.AdaptyPaywallProduct.ProductType : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.RegionCode : System.String +public readonly AdaptySDK.AdaptyPaywallProduct.Subscription : AdaptySDK.AdaptySubscription +public readonly AdaptySDK.AdaptyPaywallProduct.VendorProductId : System.String +public readonly AdaptySDK.AdaptyPlacement.ABTestName : System.String +public readonly AdaptySDK.AdaptyPlacement.AudienceName : System.String +public readonly AdaptySDK.AdaptyPlacement.Id : System.String +public readonly AdaptySDK.AdaptyPlacement.IsTrackingPurchases : System.Nullable +public readonly AdaptySDK.AdaptyPlacement.PlacementAudienceVersionId : System.String +public readonly AdaptySDK.AdaptyPlacement.Revision : System.Int64 +public readonly AdaptySDK.AdaptyPrice.Amount : System.Double +public readonly AdaptySDK.AdaptyPrice.CurrencyCode : System.String +public readonly AdaptySDK.AdaptyPrice.CurrencySymbol : System.String +public readonly AdaptySDK.AdaptyPrice.LocalizedString : System.String +public readonly AdaptySDK.AdaptyProductIdentifier.BasePlanId : System.String +public readonly AdaptySDK.AdaptyProductIdentifier.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActivatedAt : System.DateTime +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActiveIntroductoryOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActivePromotionalOfferId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ActivePromotionalOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.BillingIssueDetectedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.CancellationReason : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.ExpiresAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.Id : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsActive : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsInGracePeriod : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsLifetime : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.IsRefund : System.Boolean +public readonly AdaptySDK.AdaptyProfile+AccessLevel.OfferId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.RenewedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.StartsAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.Store : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.UnsubscribedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+AccessLevel.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+AccessLevel.WillRenew : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.IsConsumable : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.IsRefund : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.IsSandbox : System.Boolean +public readonly AdaptySDK.AdaptyProfile+NonSubscription.PurchaseId : System.String +public readonly AdaptySDK.AdaptyProfile+NonSubscription.PurchasedAt : System.DateTime +public readonly AdaptySDK.AdaptyProfile+NonSubscription.Store : System.String +public readonly AdaptySDK.AdaptyProfile+NonSubscription.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+NonSubscription.VendorTransactionId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ActivatedAt : System.DateTime +public readonly AdaptySDK.AdaptyProfile+Subscription.ActiveIntroductoryOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ActivePromotionalOfferId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ActivePromotionalOfferType : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.BillingIssueDetectedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.CancellationReason : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.ExpiresAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.IsActive : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsInGracePeriod : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsLifetime : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsRefund : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.IsSandbox : System.Boolean +public readonly AdaptySDK.AdaptyProfile+Subscription.OfferId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.RenewedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.StartsAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.Store : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.UnsubscribedAt : System.Nullable +public readonly AdaptySDK.AdaptyProfile+Subscription.VendorOriginalTransactionId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.VendorProductId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.VendorTransactionId : System.String +public readonly AdaptySDK.AdaptyProfile+Subscription.WillRenew : System.Boolean +public readonly AdaptySDK.AdaptyProfile.CustomerUserId : System.String +public readonly AdaptySDK.AdaptyProfile.ProfileId : System.String +public readonly AdaptySDK.AdaptyPurchaseParameters.IsOfferPersonalized : System.Nullable +public readonly AdaptySDK.AdaptyPurchaseParameters.SubscriptionUpdateParams : AdaptySDK.AdaptySubscriptionUpdateParameters +public readonly AdaptySDK.AdaptyPurchaseResult.AppleJWSTransaction : System.String +public readonly AdaptySDK.AdaptyPurchaseResult.GooglePurchaseToken : System.String +public readonly AdaptySDK.AdaptyPurchaseResult.Profile : AdaptySDK.AdaptyProfile +public readonly AdaptySDK.AdaptyPurchaseResult.Type : AdaptySDK.AdaptyPurchaseResultType +public readonly AdaptySDK.AdaptyRemoteConfig.Data : System.String +public readonly AdaptySDK.AdaptyRemoteConfig.Locale : System.String +public readonly AdaptySDK.AdaptySubscription.BasePlanId : System.String +public readonly AdaptySDK.AdaptySubscription.GroupIdentifier : System.String +public readonly AdaptySDK.AdaptySubscription.LocalizedPeriod : System.String +public readonly AdaptySDK.AdaptySubscription.Offer : AdaptySDK.AdaptySubscriptionOffer +public readonly AdaptySDK.AdaptySubscription.Period : AdaptySDK.AdaptySubscriptionPeriod +public readonly AdaptySDK.AdaptySubscription.RenewalType : AdaptySDK.AdaptySubscriptionRenewalType +public readonly AdaptySDK.AdaptySubscriptionOffer.Identifier : System.String +public readonly AdaptySDK.AdaptySubscriptionOffer.OfferTags : System.Collections.Generic.IReadOnlyList +public readonly AdaptySDK.AdaptySubscriptionOffer.Phases : System.Collections.Generic.IReadOnlyList +public readonly AdaptySDK.AdaptySubscriptionOffer.Type : AdaptySDK.AdaptySubscriptionOfferType +public readonly AdaptySDK.AdaptySubscriptionPeriod.NumberOfUnits : System.Int64 +public readonly AdaptySDK.AdaptySubscriptionPeriod.Unit : AdaptySDK.AdaptySubscriptionPeriodUnit +public readonly AdaptySDK.AdaptySubscriptionPhase.LocalizedNumberOfPeriods : System.String +public readonly AdaptySDK.AdaptySubscriptionPhase.LocalizedSubscriptionPeriod : System.String +public readonly AdaptySDK.AdaptySubscriptionPhase.NumberOfPeriods : System.Int32 +public readonly AdaptySDK.AdaptySubscriptionPhase.PaymentMode : AdaptySDK.AdaptyPaymentMode +public readonly AdaptySDK.AdaptySubscriptionPhase.Price : AdaptySDK.AdaptyPrice +public readonly AdaptySDK.AdaptySubscriptionPhase.SubscriptionPeriod : AdaptySDK.AdaptySubscriptionPeriod +public readonly AdaptySDK.AdaptyUIOnboardingMeta.OnboardingId : System.String +public readonly AdaptySDK.AdaptyUIOnboardingMeta.ScreenClientId : System.String +public readonly AdaptySDK.AdaptyUIOnboardingMeta.ScreenIndex : System.Int32 +public readonly AdaptySDK.AdaptyUIOnboardingMeta.ScreensTotal : System.Int32 +public sealed class AdaptySDK.AdaptyConfiguration +public sealed class AdaptySDK.AdaptyConfiguration+Builder +public sealed class AdaptySDK.AdaptyCustomAssetColor : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLinearGradient : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalImageAsset : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalImageData : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalImageFile : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalVideoAsset : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomAssetLocalVideoFile : AdaptySDK.AdaptyCustomAsset +public sealed class AdaptySDK.AdaptyCustomerIdentity +public sealed class AdaptySDK.AdaptyError +public sealed class AdaptySDK.AdaptyFlow +public sealed class AdaptySDK.AdaptyFlowPaywall +public sealed class AdaptySDK.AdaptyInstallationDetails +public sealed class AdaptySDK.AdaptyInstallationStatus +public sealed class AdaptySDK.AdaptyOnboarding +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingCompleted : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventOnboardingStarted : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventProductsScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventRegistrationScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenCompleted : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventSecondScreenPresented : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventUnknown : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsAnalyticsEventUserEmailCollected : AdaptySDK.AdaptyOnboardingsAnalyticsEvent +public sealed class AdaptySDK.AdaptyOnboardingsDatePickerParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsEmailInput : AdaptySDK.AdaptyOnboardingsInput +public sealed class AdaptySDK.AdaptyOnboardingsInputParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsMultiSelectParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsNumberInput : AdaptySDK.AdaptyOnboardingsInput +public sealed class AdaptySDK.AdaptyOnboardingsSelectParams : AdaptySDK.AdaptyOnboardingsStateUpdatedParams +public sealed class AdaptySDK.AdaptyOnboardingsTextInput : AdaptySDK.AdaptyOnboardingsInput +public sealed class AdaptySDK.AdaptyPaywallProduct +public sealed class AdaptySDK.AdaptyPlacement +public sealed class AdaptySDK.AdaptyPlacementFetchPolicy +public sealed class AdaptySDK.AdaptyPrice +public sealed class AdaptySDK.AdaptyProductIdentifier +public sealed class AdaptySDK.AdaptyProfile +public sealed class AdaptySDK.AdaptyProfile+AccessLevel +public sealed class AdaptySDK.AdaptyProfile+NonSubscription +public sealed class AdaptySDK.AdaptyProfile+Subscription +public sealed class AdaptySDK.AdaptyProfileParameters +public sealed class AdaptySDK.AdaptyProfileParameters+Builder +public sealed class AdaptySDK.AdaptyPurchaseParameters +public sealed class AdaptySDK.AdaptyPurchaseParametersBuilder +public sealed class AdaptySDK.AdaptyPurchaseResult +public sealed class AdaptySDK.AdaptyRemoteConfig +public sealed class AdaptySDK.AdaptySubscription +public sealed class AdaptySDK.AdaptySubscriptionOffer +public sealed class AdaptySDK.AdaptySubscriptionPeriod +public sealed class AdaptySDK.AdaptySubscriptionPhase +public sealed class AdaptySDK.AdaptySubscriptionUpdateParameters +public sealed class AdaptySDK.AdaptyUICreateFlowViewParameters +public sealed class AdaptySDK.AdaptyUIDialogConfiguration +public sealed class AdaptySDK.AdaptyUIFlowView +public sealed class AdaptySDK.AdaptyUIMediaCacheConfiguration +public sealed class AdaptySDK.AdaptyUIOnboardingMeta +public sealed class AdaptySDK.AdaptyUIOnboardingView +public sealed class AdaptySDK.AdaptyUIUserAction +public static AdaptySDK.Adapty.Activate(AdaptySDK.AdaptyConfiguration configuration, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Activate(AdaptySDK.Builder configurationBuilder, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.CreateWebPaywallUrl(AdaptySDK.AdaptyFlowPaywall paywall, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.CreateWebPaywallUrl(AdaptySDK.AdaptyPaywallProduct product, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetCurrentInstallationStatus(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlow(System.String placementId, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Nullable loadTimeout, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlow(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlowForDefaultAudience(System.String placementId, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetFlowForDefaultAudience(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetLogLevel(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetNativeSDKVersion(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboarding(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboarding(System.String placementId, System.String locale, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Nullable loadTimeout, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, System.String locale, AdaptySDK.AdaptyPlacementFetchPolicy fetchPolicy, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetOnboardingForDefaultAudience(System.String placementId, System.String locale, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.GetPaywallProducts(AdaptySDK.AdaptyFlow flow, System.Action, AdaptySDK.AdaptyError> completionHandler) : System.Void +public static AdaptySDK.Adapty.GetProfile(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Identify(System.String customerUserId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Identify(System.String customerUserId, System.Guid iosAppAccountToken, System.String androidObfuscatedAccountId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.IsActivated(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.LogShowFlow(AdaptySDK.AdaptyFlow flow, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.Logout(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.MakePurchase(AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyPurchaseParameters purchaseParameters, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.MakePurchase(AdaptySDK.AdaptyPaywallProduct product, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyFlowPaywall paywall, AdaptySDK.AdaptyWebPresentation openIn, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyFlowPaywall paywall, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyPaywallProduct product, AdaptySDK.AdaptyWebPresentation openIn, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.OpenWebPaywall(AdaptySDK.AdaptyPaywallProduct product, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.PresentCodeRedemptionSheet(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.ReportTransaction(System.String transactionId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.ReportTransaction(System.String transactionId, System.String variationId, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.RestorePurchases(System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetEventListener(AdaptySDK.IAdaptyEventListener listener) : System.Void +public static AdaptySDK.Adapty.SetFallback(System.String fileName, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetFlowsEventsListener(AdaptySDK.IAdaptyFlowsEventsListener listener) : System.Void +public static AdaptySDK.Adapty.SetIntegrationIdentifier(System.String key, System.String value, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetLogLevel(AdaptySDK.AdaptyLogLevel level, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.SetObserverModeResolver(AdaptySDK.IAdaptyUIObserverModeResolver resolver) : System.Void +public static AdaptySDK.Adapty.SetOnboardingsEventsListener(AdaptySDK.IAdaptyOnboardingsEventsListener listener) : System.Void +public static AdaptySDK.Adapty.SetSystemRequestsHandler(AdaptySDK.IAdaptyUISystemRequestsHandler handler) : System.Void +public static AdaptySDK.Adapty.UpdateAppStoreCollectingRefundDataConsent(System.Boolean consent, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateAppStoreRefundPreference(AdaptySDK.AdaptyRefundPreference refundPreference, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateAttribution(System.Collections.Generic.IReadOnlyDictionary attribution, System.String source, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateAttribution(System.String jsonString, System.String source, System.Action completionHandler) : System.Void +public static AdaptySDK.Adapty.UpdateProfile(AdaptySDK.AdaptyProfileParameters param, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyCustomAsset.Color(UnityEngine.Color color) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LinearGradient(UnityEngine.Gradient gradient) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalImageAsset(System.String assetId) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalImageData(System.Byte[] data) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalImageFile(System.String path) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalVideoAsset(System.String assetId) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyCustomAsset.LocalVideoFile(System.String path) : AdaptySDK.AdaptyCustomAsset +public static AdaptySDK.AdaptyPlacementFetchPolicy.ReturnCacheDataIfNotExpiredElseLoad(System.TimeSpan maxAge) : AdaptySDK.AdaptyPlacementFetchPolicy +public static AdaptySDK.AdaptyUI.CreateFlowView(AdaptySDK.AdaptyFlow flow, AdaptySDK.AdaptyUICreateFlowViewParameters optionalParameters, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.CreateFlowView(AdaptySDK.AdaptyFlow flow, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.CreateOnboardingView(AdaptySDK.AdaptyOnboarding onboarding, AdaptySDK.AdaptyWebPresentation externalUrlsPresentation, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.CreateOnboardingView(AdaptySDK.AdaptyOnboarding onboarding, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.DismissFlowView(AdaptySDK.AdaptyUIFlowView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.DismissOnboardingView(AdaptySDK.AdaptyUIOnboardingView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.OpenUrl(System.String url, AdaptySDK.AdaptyWebPresentation openIn, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentFlowView(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentFlowView(AdaptySDK.AdaptyUIFlowView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentOnboardingView(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIIOSPresentationStyle iosPresentationStyle, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.PresentOnboardingView(AdaptySDK.AdaptyUIOnboardingView view, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.RequestAppReview(System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.ShowDialog(AdaptySDK.AdaptyUIFlowView view, AdaptySDK.AdaptyUIDialogConfiguration configuration, System.Action completionHandler) : System.Void +public static AdaptySDK.AdaptyUI.ShowDialog(AdaptySDK.AdaptyUIOnboardingView view, AdaptySDK.AdaptyUIDialogConfiguration configuration, System.Action completionHandler) : System.Void +public static class AdaptySDK.Adapty +public static class AdaptySDK.AdaptyUI +public static const AdaptySDK.AdaptyErrorCode.ActivateOnceError : AdaptySDK.AdaptyErrorCode = 3005 +public static const AdaptySDK.AdaptyErrorCode.AdaptyNotInitialized : AdaptySDK.AdaptyErrorCode = 20 +public static const AdaptySDK.AdaptyErrorCode.AnalyticsDisabled : AdaptySDK.AdaptyErrorCode = 3000 +public static const AdaptySDK.AdaptyErrorCode.BadRequest : AdaptySDK.AdaptyErrorCode = 2003 +public static const AdaptySDK.AdaptyErrorCode.BillingError : AdaptySDK.AdaptyErrorCode = 106 +public static const AdaptySDK.AdaptyErrorCode.BillingNetworkError : AdaptySDK.AdaptyErrorCode = 112 +public static const AdaptySDK.AdaptyErrorCode.BillingServiceDisconnected : AdaptySDK.AdaptyErrorCode = 99 +public static const AdaptySDK.AdaptyErrorCode.BillingServiceTimeout : AdaptySDK.AdaptyErrorCode = 97 +public static const AdaptySDK.AdaptyErrorCode.BillingServiceUnavailable : AdaptySDK.AdaptyErrorCode = 102 +public static const AdaptySDK.AdaptyErrorCode.BillingUnavailable : AdaptySDK.AdaptyErrorCode = 103 +public static const AdaptySDK.AdaptyErrorCode.CantMakePayments : AdaptySDK.AdaptyErrorCode = 1003 +public static const AdaptySDK.AdaptyErrorCode.CantReadReceipt : AdaptySDK.AdaptyErrorCode = 1005 +public static const AdaptySDK.AdaptyErrorCode.ClientInvalid : AdaptySDK.AdaptyErrorCode = 1 +public static const AdaptySDK.AdaptyErrorCode.CloudServiceNetworkConnectionFailed : AdaptySDK.AdaptyErrorCode = 7 +public static const AdaptySDK.AdaptyErrorCode.CloudServicePermissionDenied : AdaptySDK.AdaptyErrorCode = 6 +public static const AdaptySDK.AdaptyErrorCode.CloudServiceRevoked : AdaptySDK.AdaptyErrorCode = 8 +public static const AdaptySDK.AdaptyErrorCode.CurrentSubscriptionToUpdateNotFoundInHistory : AdaptySDK.AdaptyErrorCode = 24 +public static const AdaptySDK.AdaptyErrorCode.DecodingFailed : AdaptySDK.AdaptyErrorCode = 2006 +public static const AdaptySDK.AdaptyErrorCode.DeveloperError : AdaptySDK.AdaptyErrorCode = 105 +public static const AdaptySDK.AdaptyErrorCode.EncodingFailed : AdaptySDK.AdaptyErrorCode = 2009 +public static const AdaptySDK.AdaptyErrorCode.FeatureNotSupported : AdaptySDK.AdaptyErrorCode = 98 +public static const AdaptySDK.AdaptyErrorCode.FetchSubscriptionStatusFailed : AdaptySDK.AdaptyErrorCode = 1020 +public static const AdaptySDK.AdaptyErrorCode.FetchTimeoutError : AdaptySDK.AdaptyErrorCode = 3101 +public static const AdaptySDK.AdaptyErrorCode.InvalidActionUrl : AdaptySDK.AdaptyErrorCode = 4107 +public static const AdaptySDK.AdaptyErrorCode.InvalidOfferIdentifier : AdaptySDK.AdaptyErrorCode = 11 +public static const AdaptySDK.AdaptyErrorCode.InvalidOfferPrice : AdaptySDK.AdaptyErrorCode = 14 +public static const AdaptySDK.AdaptyErrorCode.InvalidSignature : AdaptySDK.AdaptyErrorCode = 12 +public static const AdaptySDK.AdaptyErrorCode.ItemAlreadyOwned : AdaptySDK.AdaptyErrorCode = 107 +public static const AdaptySDK.AdaptyErrorCode.ItemNotOwned : AdaptySDK.AdaptyErrorCode = 108 +public static const AdaptySDK.AdaptyErrorCode.JsException : AdaptySDK.AdaptyErrorCode = 4105 +public static const AdaptySDK.AdaptyErrorCode.MissingOfferParams : AdaptySDK.AdaptyErrorCode = 13 +public static const AdaptySDK.AdaptyErrorCode.NavigatorNotFound : AdaptySDK.AdaptyErrorCode = 4106 +public static const AdaptySDK.AdaptyErrorCode.NetworkFailed : AdaptySDK.AdaptyErrorCode = 2005 +public static const AdaptySDK.AdaptyErrorCode.NoProductIDsFound : AdaptySDK.AdaptyErrorCode = 1000 +public static const AdaptySDK.AdaptyErrorCode.NoPurchasesToRestore : AdaptySDK.AdaptyErrorCode = 1004 +public static const AdaptySDK.AdaptyErrorCode.NotActivated : AdaptySDK.AdaptyErrorCode = 2002 +public static const AdaptySDK.AdaptyErrorCode.OperationInterrupted : AdaptySDK.AdaptyErrorCode = 9000 +public static const AdaptySDK.AdaptyErrorCode.PaymentCancelled : AdaptySDK.AdaptyErrorCode = 2 +public static const AdaptySDK.AdaptyErrorCode.PaymentInvalid : AdaptySDK.AdaptyErrorCode = 3 +public static const AdaptySDK.AdaptyErrorCode.PaymentNotAllowed : AdaptySDK.AdaptyErrorCode = 4 +public static const AdaptySDK.AdaptyErrorCode.PaymentPendingError : AdaptySDK.AdaptyErrorCode = 1050 +public static const AdaptySDK.AdaptyErrorCode.PrivacyAcknowledgementRequired : AdaptySDK.AdaptyErrorCode = 9 +public static const AdaptySDK.AdaptyErrorCode.ProductNotFound : AdaptySDK.AdaptyErrorCode = 22 +public static const AdaptySDK.AdaptyErrorCode.ProductPurchaseFailed : AdaptySDK.AdaptyErrorCode = 1006 +public static const AdaptySDK.AdaptyErrorCode.ProductRequestFailed : AdaptySDK.AdaptyErrorCode = 1002 +public static const AdaptySDK.AdaptyErrorCode.ProfileWasChanged : AdaptySDK.AdaptyErrorCode = 3006 +public static const AdaptySDK.AdaptyErrorCode.RefreshReceiptFailed : AdaptySDK.AdaptyErrorCode = 1010 +public static const AdaptySDK.AdaptyErrorCode.ServerError : AdaptySDK.AdaptyErrorCode = 2004 +public static const AdaptySDK.AdaptyErrorCode.StoreProductNotAvailable : AdaptySDK.AdaptyErrorCode = 5 +public static const AdaptySDK.AdaptyErrorCode.UnauthorizedRequestData : AdaptySDK.AdaptyErrorCode = 10 +public static const AdaptySDK.AdaptyErrorCode.UnidentifiedUserLogout : AdaptySDK.AdaptyErrorCode = 3020 +public static const AdaptySDK.AdaptyErrorCode.Unknown : AdaptySDK.AdaptyErrorCode = 0 +public static const AdaptySDK.AdaptyErrorCode.UnsupportedData : AdaptySDK.AdaptyErrorCode = 3007 +public static const AdaptySDK.AdaptyErrorCode.WrongAssetType : AdaptySDK.AdaptyErrorCode = 4104 +public static const AdaptySDK.AdaptyErrorCode.WrongParam : AdaptySDK.AdaptyErrorCode = 3001 +public static const AdaptySDK.AdaptyInstallationStatusType.Determined : AdaptySDK.AdaptyInstallationStatusType = 2 +public static const AdaptySDK.AdaptyInstallationStatusType.NotAvailable : AdaptySDK.AdaptyInstallationStatusType = 0 +public static const AdaptySDK.AdaptyInstallationStatusType.NotDetermined : AdaptySDK.AdaptyInstallationStatusType = 1 +public static const AdaptySDK.AdaptyLogLevel.Debug : AdaptySDK.AdaptyLogLevel = 4 +public static const AdaptySDK.AdaptyLogLevel.Error : AdaptySDK.AdaptyLogLevel = 0 +public static const AdaptySDK.AdaptyLogLevel.Info : AdaptySDK.AdaptyLogLevel = 2 +public static const AdaptySDK.AdaptyLogLevel.Verbose : AdaptySDK.AdaptyLogLevel = 3 +public static const AdaptySDK.AdaptyLogLevel.Warn : AdaptySDK.AdaptyLogLevel = 1 +public static const AdaptySDK.AdaptyPaymentMode.FreeTrial : AdaptySDK.AdaptyPaymentMode = 2 +public static const AdaptySDK.AdaptyPaymentMode.PayAsYouGo : AdaptySDK.AdaptyPaymentMode = 0 +public static const AdaptySDK.AdaptyPaymentMode.PayUpFront : AdaptySDK.AdaptyPaymentMode = 1 +public static const AdaptySDK.AdaptyPaymentMode.Unknown : AdaptySDK.AdaptyPaymentMode = 3 +public static const AdaptySDK.AdaptyProfileGender.Female : AdaptySDK.AdaptyProfileGender = 0 +public static const AdaptySDK.AdaptyProfileGender.Male : AdaptySDK.AdaptyProfileGender = 1 +public static const AdaptySDK.AdaptyProfileGender.Other : AdaptySDK.AdaptyProfileGender = 2 +public static const AdaptySDK.AdaptyPurchaseResultType.Pending : AdaptySDK.AdaptyPurchaseResultType = 0 +public static const AdaptySDK.AdaptyPurchaseResultType.Success : AdaptySDK.AdaptyPurchaseResultType = 2 +public static const AdaptySDK.AdaptyPurchaseResultType.UserCancelled : AdaptySDK.AdaptyPurchaseResultType = 1 +public static const AdaptySDK.AdaptyRefundPreference.Decline : AdaptySDK.AdaptyRefundPreference = 2 +public static const AdaptySDK.AdaptyRefundPreference.Grant : AdaptySDK.AdaptyRefundPreference = 1 +public static const AdaptySDK.AdaptyRefundPreference.NoPreference : AdaptySDK.AdaptyRefundPreference = 0 +public static const AdaptySDK.AdaptyServerCluster.CN : AdaptySDK.AdaptyServerCluster = 2 +public static const AdaptySDK.AdaptyServerCluster.Default : AdaptySDK.AdaptyServerCluster = 0 +public static const AdaptySDK.AdaptyServerCluster.EU : AdaptySDK.AdaptyServerCluster = 1 +public static const AdaptySDK.AdaptySubscriptionOfferType.Code : AdaptySDK.AdaptySubscriptionOfferType = 3 +public static const AdaptySDK.AdaptySubscriptionOfferType.Introductory : AdaptySDK.AdaptySubscriptionOfferType = 0 +public static const AdaptySDK.AdaptySubscriptionOfferType.Promotional : AdaptySDK.AdaptySubscriptionOfferType = 1 +public static const AdaptySDK.AdaptySubscriptionOfferType.WinBack : AdaptySDK.AdaptySubscriptionOfferType = 2 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Day : AdaptySDK.AdaptySubscriptionPeriodUnit = 0 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Month : AdaptySDK.AdaptySubscriptionPeriodUnit = 2 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Unknown : AdaptySDK.AdaptySubscriptionPeriodUnit = 4 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Week : AdaptySDK.AdaptySubscriptionPeriodUnit = 1 +public static const AdaptySDK.AdaptySubscriptionPeriodUnit.Year : AdaptySDK.AdaptySubscriptionPeriodUnit = 3 +public static const AdaptySDK.AdaptySubscriptionRenewalType.Autorenewable : AdaptySDK.AdaptySubscriptionRenewalType = 1 +public static const AdaptySDK.AdaptySubscriptionRenewalType.Prepaid : AdaptySDK.AdaptySubscriptionRenewalType = 0 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.ChargeFullPrice : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 4 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.ChargeProratedPrice : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 1 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.Deferred : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 3 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.WithTimeProration : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 0 +public static const AdaptySDK.AdaptySubscriptionUpdateReplacementMode.WithoutProration : AdaptySDK.AdaptySubscriptionUpdateReplacementMode = 2 +public static const AdaptySDK.AdaptyUIDialogActionType.Primary : AdaptySDK.AdaptyUIDialogActionType = 0 +public static const AdaptySDK.AdaptyUIDialogActionType.Secondary : AdaptySDK.AdaptyUIDialogActionType = 1 +public static const AdaptySDK.AdaptyUIIOSPresentationStyle.FullScreen : AdaptySDK.AdaptyUIIOSPresentationStyle = 0 +public static const AdaptySDK.AdaptyUIIOSPresentationStyle.PageSheet : AdaptySDK.AdaptyUIIOSPresentationStyle = 1 +public static const AdaptySDK.AdaptyUIUserActionType.Close : AdaptySDK.AdaptyUIUserActionType = 0 +public static const AdaptySDK.AdaptyUIUserActionType.Custom : AdaptySDK.AdaptyUIUserActionType = 3 +public static const AdaptySDK.AdaptyUIUserActionType.OpenUrl : AdaptySDK.AdaptyUIUserActionType = 2 +public static const AdaptySDK.AdaptyUIUserActionType.SystemBack : AdaptySDK.AdaptyUIUserActionType = 1 +public static const AdaptySDK.AdaptyWebPresentation.ExternalBrowser : AdaptySDK.AdaptyWebPresentation = 0 +public static const AdaptySDK.AdaptyWebPresentation.InAppBrowser : AdaptySDK.AdaptyWebPresentation = 1 +public static const AdaptySDK.AppTrackingTransparencyStatus.Authorized : AdaptySDK.AppTrackingTransparencyStatus = 3 +public static const AdaptySDK.AppTrackingTransparencyStatus.Denied : AdaptySDK.AppTrackingTransparencyStatus = 2 +public static const AdaptySDK.AppTrackingTransparencyStatus.NotDetermined : AdaptySDK.AppTrackingTransparencyStatus = 0 +public static const AdaptySDK.AppTrackingTransparencyStatus.Restricted : AdaptySDK.AppTrackingTransparencyStatus = 1 +public static readonly AdaptySDK.Adapty.SDKVersion : System.String +public static readonly AdaptySDK.AdaptyPlacementFetchPolicy.Default : AdaptySDK.AdaptyPlacementFetchPolicy +public static readonly AdaptySDK.AdaptyPlacementFetchPolicy.ReloadRevalidatingCacheData : AdaptySDK.AdaptyPlacementFetchPolicy +public static readonly AdaptySDK.AdaptyPlacementFetchPolicy.ReturnCacheDataElseLoad : AdaptySDK.AdaptyPlacementFetchPolicy \ No newline at end of file diff --git a/tests/shared/Fixtures/approved/purchase-result-cancelled.android.approved.txt b/tests/shared/Fixtures/approved/purchase-result-cancelled.android.approved.txt new file mode 100644 index 0000000..9067ebc --- /dev/null +++ b/tests/shared/Fixtures/approved/purchase-result-cancelled.android.approved.txt @@ -0,0 +1,7 @@ +{ + "$type": "AdaptyPurchaseResult", + "AppleJWSTransaction": null, + "GooglePurchaseToken": null, + "Profile": null, + "Type": "UserCancelled (1)" +} diff --git a/tests/shared/Fixtures/approved/purchase-result-cancelled.editor.approved.txt b/tests/shared/Fixtures/approved/purchase-result-cancelled.editor.approved.txt new file mode 100644 index 0000000..9067ebc --- /dev/null +++ b/tests/shared/Fixtures/approved/purchase-result-cancelled.editor.approved.txt @@ -0,0 +1,7 @@ +{ + "$type": "AdaptyPurchaseResult", + "AppleJWSTransaction": null, + "GooglePurchaseToken": null, + "Profile": null, + "Type": "UserCancelled (1)" +} diff --git a/tests/shared/Fixtures/approved/purchase-result-cancelled.ios.approved.txt b/tests/shared/Fixtures/approved/purchase-result-cancelled.ios.approved.txt new file mode 100644 index 0000000..9067ebc --- /dev/null +++ b/tests/shared/Fixtures/approved/purchase-result-cancelled.ios.approved.txt @@ -0,0 +1,7 @@ +{ + "$type": "AdaptyPurchaseResult", + "AppleJWSTransaction": null, + "GooglePurchaseToken": null, + "Profile": null, + "Type": "UserCancelled (1)" +} diff --git a/tests/shared/Fixtures/approved/purchase-result-pending.android.approved.txt b/tests/shared/Fixtures/approved/purchase-result-pending.android.approved.txt new file mode 100644 index 0000000..dc6756b --- /dev/null +++ b/tests/shared/Fixtures/approved/purchase-result-pending.android.approved.txt @@ -0,0 +1,7 @@ +{ + "$type": "AdaptyPurchaseResult", + "AppleJWSTransaction": null, + "GooglePurchaseToken": null, + "Profile": null, + "Type": "Pending (0)" +} diff --git a/tests/shared/Fixtures/approved/purchase-result-pending.editor.approved.txt b/tests/shared/Fixtures/approved/purchase-result-pending.editor.approved.txt new file mode 100644 index 0000000..dc6756b --- /dev/null +++ b/tests/shared/Fixtures/approved/purchase-result-pending.editor.approved.txt @@ -0,0 +1,7 @@ +{ + "$type": "AdaptyPurchaseResult", + "AppleJWSTransaction": null, + "GooglePurchaseToken": null, + "Profile": null, + "Type": "Pending (0)" +} diff --git a/tests/shared/Fixtures/approved/purchase-result-pending.ios.approved.txt b/tests/shared/Fixtures/approved/purchase-result-pending.ios.approved.txt new file mode 100644 index 0000000..dc6756b --- /dev/null +++ b/tests/shared/Fixtures/approved/purchase-result-pending.ios.approved.txt @@ -0,0 +1,7 @@ +{ + "$type": "AdaptyPurchaseResult", + "AppleJWSTransaction": null, + "GooglePurchaseToken": null, + "Profile": null, + "Type": "Pending (0)" +} diff --git a/tests/shared/Fixtures/approved/purchase-result-success.android.approved.txt b/tests/shared/Fixtures/approved/purchase-result-success.android.approved.txt new file mode 100644 index 0000000..15049fd --- /dev/null +++ b/tests/shared/Fixtures/approved/purchase-result-success.android.approved.txt @@ -0,0 +1,24 @@ +{ + "$type": "AdaptyPurchaseResult", + "AppleJWSTransaction": "eyJhbGciOiJFUzI1NiJ9.payload.signature", + "GooglePurchaseToken": "gpa.1234-5678-9012-34567", + "Profile": { + "$type": "AdaptyProfile", + "AccessLevels (property)": {}, + "AppliedAttributionSources (property)": [], + "CustomAttributes (property)": {}, + "CustomerUserId": null, + "IsTestUser": false, + "NonSubscriptions (property)": {}, + "ProfileId": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "SegmentId": "8f14e45fceea167a", + "Subscriptions (property)": {}, + "Version": 1753876800000, + "_AccessLevels": {}, + "_AppliedAttributionSources": [], + "_CustomAttributes": {}, + "_NonSubscriptions": {}, + "_Subscriptions": {} + }, + "Type": "Success (2)" +} diff --git a/tests/shared/Fixtures/approved/purchase-result-success.editor.approved.txt b/tests/shared/Fixtures/approved/purchase-result-success.editor.approved.txt new file mode 100644 index 0000000..15049fd --- /dev/null +++ b/tests/shared/Fixtures/approved/purchase-result-success.editor.approved.txt @@ -0,0 +1,24 @@ +{ + "$type": "AdaptyPurchaseResult", + "AppleJWSTransaction": "eyJhbGciOiJFUzI1NiJ9.payload.signature", + "GooglePurchaseToken": "gpa.1234-5678-9012-34567", + "Profile": { + "$type": "AdaptyProfile", + "AccessLevels (property)": {}, + "AppliedAttributionSources (property)": [], + "CustomAttributes (property)": {}, + "CustomerUserId": null, + "IsTestUser": false, + "NonSubscriptions (property)": {}, + "ProfileId": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "SegmentId": "8f14e45fceea167a", + "Subscriptions (property)": {}, + "Version": 1753876800000, + "_AccessLevels": {}, + "_AppliedAttributionSources": [], + "_CustomAttributes": {}, + "_NonSubscriptions": {}, + "_Subscriptions": {} + }, + "Type": "Success (2)" +} diff --git a/tests/shared/Fixtures/approved/purchase-result-success.ios.approved.txt b/tests/shared/Fixtures/approved/purchase-result-success.ios.approved.txt new file mode 100644 index 0000000..15049fd --- /dev/null +++ b/tests/shared/Fixtures/approved/purchase-result-success.ios.approved.txt @@ -0,0 +1,24 @@ +{ + "$type": "AdaptyPurchaseResult", + "AppleJWSTransaction": "eyJhbGciOiJFUzI1NiJ9.payload.signature", + "GooglePurchaseToken": "gpa.1234-5678-9012-34567", + "Profile": { + "$type": "AdaptyProfile", + "AccessLevels (property)": {}, + "AppliedAttributionSources (property)": [], + "CustomAttributes (property)": {}, + "CustomerUserId": null, + "IsTestUser": false, + "NonSubscriptions (property)": {}, + "ProfileId": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "SegmentId": "8f14e45fceea167a", + "Subscriptions (property)": {}, + "Version": 1753876800000, + "_AccessLevels": {}, + "_AppliedAttributionSources": [], + "_CustomAttributes": {}, + "_NonSubscriptions": {}, + "_Subscriptions": {} + }, + "Type": "Success (2)" +} diff --git a/tests/shared/Fixtures/approved/request-configuration-default-cluster-kids.ios.approved.txt b/tests/shared/Fixtures/approved/request-configuration-default-cluster-kids.ios.approved.txt new file mode 100644 index 0000000..bc1a41b --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration-default-cluster-kids.ios.approved.txt @@ -0,0 +1,13 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": true, + "backend_proxy_port": 0, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": false, + "log_level": "error", + "server_cluster": "default" +} diff --git a/tests/shared/Fixtures/approved/request-configuration-default-cluster.android.approved.txt b/tests/shared/Fixtures/approved/request-configuration-default-cluster.android.approved.txt new file mode 100644 index 0000000..0f0e8d4 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration-default-cluster.android.approved.txt @@ -0,0 +1,13 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": false, + "backend_proxy_port": 0, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": false, + "log_level": "error", + "server_cluster": "default" +} diff --git a/tests/shared/Fixtures/approved/request-configuration-default-cluster.editor.approved.txt b/tests/shared/Fixtures/approved/request-configuration-default-cluster.editor.approved.txt new file mode 100644 index 0000000..0f0e8d4 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration-default-cluster.editor.approved.txt @@ -0,0 +1,13 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": false, + "backend_proxy_port": 0, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": false, + "log_level": "error", + "server_cluster": "default" +} diff --git a/tests/shared/Fixtures/approved/request-configuration-default-cluster.ios.approved.txt b/tests/shared/Fixtures/approved/request-configuration-default-cluster.ios.approved.txt new file mode 100644 index 0000000..0f0e8d4 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration-default-cluster.ios.approved.txt @@ -0,0 +1,13 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": false, + "backend_proxy_port": 0, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": false, + "log_level": "error", + "server_cluster": "default" +} diff --git a/tests/shared/Fixtures/approved/request-configuration-empty-identity-kids.ios.approved.txt b/tests/shared/Fixtures/approved/request-configuration-empty-identity-kids.ios.approved.txt new file mode 100644 index 0000000..3444bc1 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration-empty-identity-kids.ios.approved.txt @@ -0,0 +1,13 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": true, + "backend_proxy_port": 0, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "customer_user_id": "user-1", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": false, + "log_level": "error" +} diff --git a/tests/shared/Fixtures/approved/request-configuration-empty-identity.android.approved.txt b/tests/shared/Fixtures/approved/request-configuration-empty-identity.android.approved.txt new file mode 100644 index 0000000..57b6324 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration-empty-identity.android.approved.txt @@ -0,0 +1,13 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": false, + "backend_proxy_port": 0, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "customer_user_id": "user-1", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": false, + "log_level": "error" +} diff --git a/tests/shared/Fixtures/approved/request-configuration-empty-identity.editor.approved.txt b/tests/shared/Fixtures/approved/request-configuration-empty-identity.editor.approved.txt new file mode 100644 index 0000000..57b6324 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration-empty-identity.editor.approved.txt @@ -0,0 +1,13 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": false, + "backend_proxy_port": 0, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "customer_user_id": "user-1", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": false, + "log_level": "error" +} diff --git a/tests/shared/Fixtures/approved/request-configuration-empty-identity.ios.approved.txt b/tests/shared/Fixtures/approved/request-configuration-empty-identity.ios.approved.txt new file mode 100644 index 0000000..57b6324 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration-empty-identity.ios.approved.txt @@ -0,0 +1,13 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": false, + "backend_proxy_port": 0, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "customer_user_id": "user-1", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": false, + "log_level": "error" +} diff --git a/tests/shared/Fixtures/approved/request-configuration-kids.ios.approved.txt b/tests/shared/Fixtures/approved/request-configuration-kids.ios.approved.txt new file mode 100644 index 0000000..9c419dc --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration-kids.ios.approved.txt @@ -0,0 +1,24 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": true, + "backend_proxy_host": "proxy.example.com", + "backend_proxy_port": 8080, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "customer_identity_parameters": { + "obfuscated_account_id": "obfuscated-1" + }, + "customer_user_id": "user-1", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": true, + "log_level": "error", + "media_cache": { + "disk_storage_size_limit": 300, + "memory_storage_count_limit": 200, + "memory_storage_total_cost_limit": 100 + }, + "observer_mode": true, + "server_cluster": "eu" +} diff --git a/tests/shared/Fixtures/approved/request-configuration.android.approved.txt b/tests/shared/Fixtures/approved/request-configuration.android.approved.txt new file mode 100644 index 0000000..da75c6a --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration.android.approved.txt @@ -0,0 +1,24 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": false, + "backend_proxy_host": "proxy.example.com", + "backend_proxy_port": 8080, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "customer_identity_parameters": { + "obfuscated_account_id": "obfuscated-1" + }, + "customer_user_id": "user-1", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": true, + "log_level": "error", + "media_cache": { + "disk_storage_size_limit": 300, + "memory_storage_count_limit": 200, + "memory_storage_total_cost_limit": 100 + }, + "observer_mode": true, + "server_cluster": "eu" +} diff --git a/tests/shared/Fixtures/approved/request-configuration.editor.approved.txt b/tests/shared/Fixtures/approved/request-configuration.editor.approved.txt new file mode 100644 index 0000000..da75c6a --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration.editor.approved.txt @@ -0,0 +1,24 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": false, + "backend_proxy_host": "proxy.example.com", + "backend_proxy_port": 8080, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "customer_identity_parameters": { + "obfuscated_account_id": "obfuscated-1" + }, + "customer_user_id": "user-1", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": true, + "log_level": "error", + "media_cache": { + "disk_storage_size_limit": 300, + "memory_storage_count_limit": 200, + "memory_storage_total_cost_limit": 100 + }, + "observer_mode": true, + "server_cluster": "eu" +} diff --git a/tests/shared/Fixtures/approved/request-configuration.ios.approved.txt b/tests/shared/Fixtures/approved/request-configuration.ios.approved.txt new file mode 100644 index 0000000..da75c6a --- /dev/null +++ b/tests/shared/Fixtures/approved/request-configuration.ios.approved.txt @@ -0,0 +1,24 @@ +{ + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": false, + "backend_proxy_host": "proxy.example.com", + "backend_proxy_port": 8080, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "customer_identity_parameters": { + "obfuscated_account_id": "obfuscated-1" + }, + "customer_user_id": "user-1", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": true, + "log_level": "error", + "media_cache": { + "disk_storage_size_limit": 300, + "memory_storage_count_limit": 200, + "memory_storage_total_cost_limit": 100 + }, + "observer_mode": true, + "server_cluster": "eu" +} diff --git a/tests/shared/Fixtures/approved/request-custom-assets.android.approved.txt b/tests/shared/Fixtures/approved/request-custom-assets.android.approved.txt new file mode 100644 index 0000000..bc0730b --- /dev/null +++ b/tests/shared/Fixtures/approved/request-custom-assets.android.approved.txt @@ -0,0 +1,56 @@ +[ + { + "id": "hero_data", + "type": "image", + "value": "AQID+g==" + }, + { + "asset_id": "hero-asset-id", + "id": "hero_asset", + "type": "image" + }, + { + "id": "hero_file", + "path": "jar:file:///stub/dataPath!/assets/images/hero.png", + "type": "image" + }, + { + "asset_id": "clip-asset-id", + "id": "clip_asset", + "type": "video" + }, + { + "id": "clip_file", + "path": "jar:file:///stub/dataPath!/assets/videos/clip.mp4", + "type": "video" + }, + { + "id": "accent", + "type": "color", + "value": "#336699FF" + }, + { + "id": "backdrop", + "points": { + "x0": 0, + "x1": 1, + "y0": 0, + "y1": 0 + }, + "type": "linear-gradient", + "values": [ + { + "color": "#FF0000FF", + "p": 0 + }, + { + "color": "#BF004080", + "p": 0.25 + }, + { + "color": "#0000FF00", + "p": 1 + } + ] + } +] diff --git a/tests/shared/Fixtures/approved/request-custom-assets.editor.approved.txt b/tests/shared/Fixtures/approved/request-custom-assets.editor.approved.txt new file mode 100644 index 0000000..f273db0 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-custom-assets.editor.approved.txt @@ -0,0 +1,56 @@ +[ + { + "id": "hero_data", + "type": "image", + "value": "AQID+g==" + }, + { + "asset_id": "hero-asset-id", + "id": "hero_asset", + "type": "image" + }, + { + "id": "hero_file", + "path": "images/hero.png", + "type": "image" + }, + { + "asset_id": "clip-asset-id", + "id": "clip_asset", + "type": "video" + }, + { + "id": "clip_file", + "path": "videos/clip.mp4", + "type": "video" + }, + { + "id": "accent", + "type": "color", + "value": "#336699FF" + }, + { + "id": "backdrop", + "points": { + "x0": 0, + "x1": 1, + "y0": 0, + "y1": 0 + }, + "type": "linear-gradient", + "values": [ + { + "color": "#FF0000FF", + "p": 0 + }, + { + "color": "#BF004080", + "p": 0.25 + }, + { + "color": "#0000FF00", + "p": 1 + } + ] + } +] diff --git a/tests/shared/Fixtures/approved/request-custom-assets.ios.approved.txt b/tests/shared/Fixtures/approved/request-custom-assets.ios.approved.txt new file mode 100644 index 0000000..814ba40 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-custom-assets.ios.approved.txt @@ -0,0 +1,56 @@ +[ + { + "id": "hero_data", + "type": "image", + "value": "AQID+g==" + }, + { + "asset_id": "hero-asset-id", + "id": "hero_asset", + "type": "image" + }, + { + "id": "hero_file", + "path": "/stub/dataPath/Raw/images/hero.png", + "type": "image" + }, + { + "asset_id": "clip-asset-id", + "id": "clip_asset", + "type": "video" + }, + { + "id": "clip_file", + "path": "/stub/dataPath/Raw/videos/clip.mp4", + "type": "video" + }, + { + "id": "accent", + "type": "color", + "value": "#336699FF" + }, + { + "id": "backdrop", + "points": { + "x0": 0, + "x1": 1, + "y0": 0, + "y1": 0 + }, + "type": "linear-gradient", + "values": [ + { + "color": "#FF0000FF", + "p": 0 + }, + { + "color": "#BF004080", + "p": 0.25 + }, + { + "color": "#0000FF00", + "p": 1 + } + ] + } +] diff --git a/tests/shared/Fixtures/approved/request-dialog-configuration-minimal.android.approved.txt b/tests/shared/Fixtures/approved/request-dialog-configuration-minimal.android.approved.txt new file mode 100644 index 0000000..e7e1948 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-dialog-configuration-minimal.android.approved.txt @@ -0,0 +1,3 @@ +{ + "default_action_title": "OK" +} diff --git a/tests/shared/Fixtures/approved/request-dialog-configuration-minimal.editor.approved.txt b/tests/shared/Fixtures/approved/request-dialog-configuration-minimal.editor.approved.txt new file mode 100644 index 0000000..e7e1948 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-dialog-configuration-minimal.editor.approved.txt @@ -0,0 +1,3 @@ +{ + "default_action_title": "OK" +} diff --git a/tests/shared/Fixtures/approved/request-dialog-configuration-minimal.ios.approved.txt b/tests/shared/Fixtures/approved/request-dialog-configuration-minimal.ios.approved.txt new file mode 100644 index 0000000..e7e1948 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-dialog-configuration-minimal.ios.approved.txt @@ -0,0 +1,3 @@ +{ + "default_action_title": "OK" +} diff --git a/tests/shared/Fixtures/approved/request-dialog-configuration.android.approved.txt b/tests/shared/Fixtures/approved/request-dialog-configuration.android.approved.txt new file mode 100644 index 0000000..7462375 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-dialog-configuration.android.approved.txt @@ -0,0 +1,6 @@ +{ + "content": "You keep access until the end of the period.", + "default_action_title": "Keep", + "secondary_action_title": "Cancel", + "title": "Cancel subscription?" +} diff --git a/tests/shared/Fixtures/approved/request-dialog-configuration.editor.approved.txt b/tests/shared/Fixtures/approved/request-dialog-configuration.editor.approved.txt new file mode 100644 index 0000000..7462375 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-dialog-configuration.editor.approved.txt @@ -0,0 +1,6 @@ +{ + "content": "You keep access until the end of the period.", + "default_action_title": "Keep", + "secondary_action_title": "Cancel", + "title": "Cancel subscription?" +} diff --git a/tests/shared/Fixtures/approved/request-dialog-configuration.ios.approved.txt b/tests/shared/Fixtures/approved/request-dialog-configuration.ios.approved.txt new file mode 100644 index 0000000..7462375 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-dialog-configuration.ios.approved.txt @@ -0,0 +1,6 @@ +{ + "content": "You keep access until the end of the period.", + "default_action_title": "Keep", + "secondary_action_title": "Cancel", + "title": "Cancel subscription?" +} diff --git a/tests/shared/Fixtures/approved/request-fetch-policy-default.android.approved.txt b/tests/shared/Fixtures/approved/request-fetch-policy-default.android.approved.txt new file mode 100644 index 0000000..3537085 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-fetch-policy-default.android.approved.txt @@ -0,0 +1,3 @@ +{ + "type": "reload_revalidating_cache_data" +} diff --git a/tests/shared/Fixtures/approved/request-fetch-policy-default.editor.approved.txt b/tests/shared/Fixtures/approved/request-fetch-policy-default.editor.approved.txt new file mode 100644 index 0000000..3537085 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-fetch-policy-default.editor.approved.txt @@ -0,0 +1,3 @@ +{ + "type": "reload_revalidating_cache_data" +} diff --git a/tests/shared/Fixtures/approved/request-fetch-policy-default.ios.approved.txt b/tests/shared/Fixtures/approved/request-fetch-policy-default.ios.approved.txt new file mode 100644 index 0000000..3537085 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-fetch-policy-default.ios.approved.txt @@ -0,0 +1,3 @@ +{ + "type": "reload_revalidating_cache_data" +} diff --git a/tests/shared/Fixtures/approved/request-fetch-policy-max-age.android.approved.txt b/tests/shared/Fixtures/approved/request-fetch-policy-max-age.android.approved.txt new file mode 100644 index 0000000..a4c23a6 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-fetch-policy-max-age.android.approved.txt @@ -0,0 +1,4 @@ +{ + "max_age": 90, + "type": "return_cache_data_if_not_expired_else_load" +} diff --git a/tests/shared/Fixtures/approved/request-fetch-policy-max-age.editor.approved.txt b/tests/shared/Fixtures/approved/request-fetch-policy-max-age.editor.approved.txt new file mode 100644 index 0000000..a4c23a6 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-fetch-policy-max-age.editor.approved.txt @@ -0,0 +1,4 @@ +{ + "max_age": 90, + "type": "return_cache_data_if_not_expired_else_load" +} diff --git a/tests/shared/Fixtures/approved/request-fetch-policy-max-age.ios.approved.txt b/tests/shared/Fixtures/approved/request-fetch-policy-max-age.ios.approved.txt new file mode 100644 index 0000000..a4c23a6 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-fetch-policy-max-age.ios.approved.txt @@ -0,0 +1,4 @@ +{ + "max_age": 90, + "type": "return_cache_data_if_not_expired_else_load" +} diff --git a/tests/shared/Fixtures/approved/request-flow-full.android.approved.txt b/tests/shared/Fixtures/approved/request-flow-full.android.approved.txt new file mode 100644 index 0000000..e5061ad --- /dev/null +++ b/tests/shared/Fixtures/approved/request-flow-full.android.approved.txt @@ -0,0 +1,59 @@ +{ + "flow_id": "flow-0001", + "flow_name": "Winter Flow", + "flow_version_id": "flow-version-0001", + "payload_data": "{\"custom\":\"payload\"}", + "placement": { + "ab_test_name": "winter_test", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": true, + "placement_audience_version_id": "pav-0001", + "revision": 7 + }, + "remote_configs": [ + { + "data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "lang": "en" + }, + { + "data": "{\"title\":\"Hazte premium\"}", + "lang": "es" + } + ], + "response_created_at": 1753876800000, + "variation_id": "variation-0001", + "variations": [ + { + "paywall_id": "paywall-0001", + "paywall_name": "Winter Paywall", + "placement": { + "ab_test_name": "winter_test", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": true, + "placement_audience_version_id": "pav-0001", + "revision": 7 + }, + "products": [ + { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-1", + "base_plan_id": "base-plan-1", + "flow_product_id": "flow-product-1", + "offer_id": "offer-1", + "product_type": "subscription", + "vendor_product_id": "com.adapty.sample.monthly" + }, + { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-2", + "product_type": "subscription", + "vendor_product_id": "com.adapty.sample.yearly" + } + ], + "variation_id": "variation-0001", + "web_purchase_url": "https://pay.adapty.io/checkout/abc" + } + ] +} diff --git a/tests/shared/Fixtures/approved/request-flow-full.editor.approved.txt b/tests/shared/Fixtures/approved/request-flow-full.editor.approved.txt new file mode 100644 index 0000000..8832e57 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-flow-full.editor.approved.txt @@ -0,0 +1,57 @@ +{ + "flow_id": "flow-0001", + "flow_name": "Winter Flow", + "flow_version_id": "flow-version-0001", + "payload_data": "{\"custom\":\"payload\"}", + "placement": { + "ab_test_name": "winter_test", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": true, + "placement_audience_version_id": "pav-0001", + "revision": 7 + }, + "remote_configs": [ + { + "data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "lang": "en" + }, + { + "data": "{\"title\":\"Hazte premium\"}", + "lang": "es" + } + ], + "response_created_at": 1753876800000, + "variation_id": "variation-0001", + "variations": [ + { + "paywall_id": "paywall-0001", + "paywall_name": "Winter Paywall", + "placement": { + "ab_test_name": "winter_test", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": true, + "placement_audience_version_id": "pav-0001", + "revision": 7 + }, + "products": [ + { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-1", + "flow_product_id": "flow-product-1", + "product_type": "subscription", + "vendor_product_id": "com.adapty.sample.monthly" + }, + { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-2", + "product_type": "subscription", + "vendor_product_id": "com.adapty.sample.yearly" + } + ], + "variation_id": "variation-0001", + "web_purchase_url": "https://pay.adapty.io/checkout/abc" + } + ] +} diff --git a/tests/shared/Fixtures/approved/request-flow-full.ios.approved.txt b/tests/shared/Fixtures/approved/request-flow-full.ios.approved.txt new file mode 100644 index 0000000..563819f --- /dev/null +++ b/tests/shared/Fixtures/approved/request-flow-full.ios.approved.txt @@ -0,0 +1,59 @@ +{ + "flow_id": "flow-0001", + "flow_name": "Winter Flow", + "flow_version_id": "flow-version-0001", + "payload_data": "{\"custom\":\"payload\"}", + "placement": { + "ab_test_name": "winter_test", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": true, + "placement_audience_version_id": "pav-0001", + "revision": 7 + }, + "remote_configs": [ + { + "data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}", + "lang": "en" + }, + { + "data": "{\"title\":\"Hazte premium\"}", + "lang": "es" + } + ], + "response_created_at": 1753876800000, + "variation_id": "variation-0001", + "variations": [ + { + "paywall_id": "paywall-0001", + "paywall_name": "Winter Paywall", + "placement": { + "ab_test_name": "winter_test", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": true, + "placement_audience_version_id": "pav-0001", + "revision": 7 + }, + "products": [ + { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-1", + "flow_product_id": "flow-product-1", + "product_type": "subscription", + "promotional_offer_id": "promo-1", + "vendor_product_id": "com.adapty.sample.monthly", + "win_back_offer_id": "winback-1" + }, + { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-2", + "product_type": "subscription", + "vendor_product_id": "com.adapty.sample.yearly" + } + ], + "variation_id": "variation-0001", + "web_purchase_url": "https://pay.adapty.io/checkout/abc" + } + ] +} diff --git a/tests/shared/Fixtures/approved/request-flow-minimal.android.approved.txt b/tests/shared/Fixtures/approved/request-flow-minimal.android.approved.txt new file mode 100644 index 0000000..dee6a90 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-flow-minimal.android.approved.txt @@ -0,0 +1,16 @@ +{ + "flow_id": "flow-0002", + "flow_name": "Minimal Flow", + "placement": { + "ab_test_name": "default", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0002", + "revision": 1 + }, + "remote_configs": [], + "response_created_at": 1753876800000, + "variation_id": "variation-0002", + "variations": [] +} diff --git a/tests/shared/Fixtures/approved/request-flow-minimal.editor.approved.txt b/tests/shared/Fixtures/approved/request-flow-minimal.editor.approved.txt new file mode 100644 index 0000000..dee6a90 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-flow-minimal.editor.approved.txt @@ -0,0 +1,16 @@ +{ + "flow_id": "flow-0002", + "flow_name": "Minimal Flow", + "placement": { + "ab_test_name": "default", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0002", + "revision": 1 + }, + "remote_configs": [], + "response_created_at": 1753876800000, + "variation_id": "variation-0002", + "variations": [] +} diff --git a/tests/shared/Fixtures/approved/request-flow-minimal.ios.approved.txt b/tests/shared/Fixtures/approved/request-flow-minimal.ios.approved.txt new file mode 100644 index 0000000..dee6a90 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-flow-minimal.ios.approved.txt @@ -0,0 +1,16 @@ +{ + "flow_id": "flow-0002", + "flow_name": "Minimal Flow", + "placement": { + "ab_test_name": "default", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0002", + "revision": 1 + }, + "remote_configs": [], + "response_created_at": 1753876800000, + "variation_id": "variation-0002", + "variations": [] +} diff --git a/tests/shared/Fixtures/approved/request-onboarding-full.android.approved.txt b/tests/shared/Fixtures/approved/request-onboarding-full.android.approved.txt new file mode 100644 index 0000000..961a6e8 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-onboarding-full.android.approved.txt @@ -0,0 +1,23 @@ +{ + "onboarding_builder": { + "config_url": "https://cdn.adapty.io/onboardings/onboarding-0001.json" + }, + "onboarding_id": "onboarding-0001", + "onboarding_name": "Welcome Onboarding", + "payload_data": "{\"custom\":\"onboarding payload\"}", + "placement": { + "ab_test_name": "onboarding_test", + "audience_name": "All Users", + "developer_id": "welcome", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0002", + "revision": 3 + }, + "remote_config": { + "data": "{\"steps\":3,\"skippable\":false}", + "lang": "en" + }, + "request_locale": "en", + "response_created_at": 1754006400, + "variation_id": "variation-0002" +} diff --git a/tests/shared/Fixtures/approved/request-onboarding-full.editor.approved.txt b/tests/shared/Fixtures/approved/request-onboarding-full.editor.approved.txt new file mode 100644 index 0000000..961a6e8 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-onboarding-full.editor.approved.txt @@ -0,0 +1,23 @@ +{ + "onboarding_builder": { + "config_url": "https://cdn.adapty.io/onboardings/onboarding-0001.json" + }, + "onboarding_id": "onboarding-0001", + "onboarding_name": "Welcome Onboarding", + "payload_data": "{\"custom\":\"onboarding payload\"}", + "placement": { + "ab_test_name": "onboarding_test", + "audience_name": "All Users", + "developer_id": "welcome", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0002", + "revision": 3 + }, + "remote_config": { + "data": "{\"steps\":3,\"skippable\":false}", + "lang": "en" + }, + "request_locale": "en", + "response_created_at": 1754006400, + "variation_id": "variation-0002" +} diff --git a/tests/shared/Fixtures/approved/request-onboarding-full.ios.approved.txt b/tests/shared/Fixtures/approved/request-onboarding-full.ios.approved.txt new file mode 100644 index 0000000..961a6e8 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-onboarding-full.ios.approved.txt @@ -0,0 +1,23 @@ +{ + "onboarding_builder": { + "config_url": "https://cdn.adapty.io/onboardings/onboarding-0001.json" + }, + "onboarding_id": "onboarding-0001", + "onboarding_name": "Welcome Onboarding", + "payload_data": "{\"custom\":\"onboarding payload\"}", + "placement": { + "ab_test_name": "onboarding_test", + "audience_name": "All Users", + "developer_id": "welcome", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0002", + "revision": 3 + }, + "remote_config": { + "data": "{\"steps\":3,\"skippable\":false}", + "lang": "en" + }, + "request_locale": "en", + "response_created_at": 1754006400, + "variation_id": "variation-0002" +} diff --git a/tests/shared/Fixtures/approved/request-onboarding-minimal.android.approved.txt b/tests/shared/Fixtures/approved/request-onboarding-minimal.android.approved.txt new file mode 100644 index 0000000..2652ae8 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-onboarding-minimal.android.approved.txt @@ -0,0 +1,18 @@ +{ + "onboarding_builder": { + "config_url": "https://cdn.adapty.io/onboardings/onboarding-0002.json" + }, + "onboarding_id": "onboarding-0002", + "onboarding_name": "Short Onboarding", + "placement": { + "ab_test_name": "onboarding_test", + "audience_name": "All Users", + "developer_id": "welcome", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0003", + "revision": 1 + }, + "request_locale": "es", + "response_created_at": 1754006401, + "variation_id": "variation-0003" +} diff --git a/tests/shared/Fixtures/approved/request-onboarding-minimal.editor.approved.txt b/tests/shared/Fixtures/approved/request-onboarding-minimal.editor.approved.txt new file mode 100644 index 0000000..2652ae8 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-onboarding-minimal.editor.approved.txt @@ -0,0 +1,18 @@ +{ + "onboarding_builder": { + "config_url": "https://cdn.adapty.io/onboardings/onboarding-0002.json" + }, + "onboarding_id": "onboarding-0002", + "onboarding_name": "Short Onboarding", + "placement": { + "ab_test_name": "onboarding_test", + "audience_name": "All Users", + "developer_id": "welcome", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0003", + "revision": 1 + }, + "request_locale": "es", + "response_created_at": 1754006401, + "variation_id": "variation-0003" +} diff --git a/tests/shared/Fixtures/approved/request-onboarding-minimal.ios.approved.txt b/tests/shared/Fixtures/approved/request-onboarding-minimal.ios.approved.txt new file mode 100644 index 0000000..2652ae8 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-onboarding-minimal.ios.approved.txt @@ -0,0 +1,18 @@ +{ + "onboarding_builder": { + "config_url": "https://cdn.adapty.io/onboardings/onboarding-0002.json" + }, + "onboarding_id": "onboarding-0002", + "onboarding_name": "Short Onboarding", + "placement": { + "ab_test_name": "onboarding_test", + "audience_name": "All Users", + "developer_id": "welcome", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0003", + "revision": 1 + }, + "request_locale": "es", + "response_created_at": 1754006401, + "variation_id": "variation-0003" +} diff --git a/tests/shared/Fixtures/approved/request-product-identifier-no-base-plan.android.approved.txt b/tests/shared/Fixtures/approved/request-product-identifier-no-base-plan.android.approved.txt new file mode 100644 index 0000000..f5cf71e --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-identifier-no-base-plan.android.approved.txt @@ -0,0 +1,4 @@ +{ + "adapty_product_id": "adapty-product-2", + "vendor_product_id": "com.adapty.sample.lifetime" +} diff --git a/tests/shared/Fixtures/approved/request-product-identifier-no-base-plan.editor.approved.txt b/tests/shared/Fixtures/approved/request-product-identifier-no-base-plan.editor.approved.txt new file mode 100644 index 0000000..f5cf71e --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-identifier-no-base-plan.editor.approved.txt @@ -0,0 +1,4 @@ +{ + "adapty_product_id": "adapty-product-2", + "vendor_product_id": "com.adapty.sample.lifetime" +} diff --git a/tests/shared/Fixtures/approved/request-product-identifier-no-base-plan.ios.approved.txt b/tests/shared/Fixtures/approved/request-product-identifier-no-base-plan.ios.approved.txt new file mode 100644 index 0000000..f5cf71e --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-identifier-no-base-plan.ios.approved.txt @@ -0,0 +1,4 @@ +{ + "adapty_product_id": "adapty-product-2", + "vendor_product_id": "com.adapty.sample.lifetime" +} diff --git a/tests/shared/Fixtures/approved/request-product-identifier.android.approved.txt b/tests/shared/Fixtures/approved/request-product-identifier.android.approved.txt new file mode 100644 index 0000000..02da221 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-identifier.android.approved.txt @@ -0,0 +1,5 @@ +{ + "adapty_product_id": "adapty-product-1", + "base_plan_id": "monthly-base-plan", + "vendor_product_id": "com.adapty.sample.monthly" +} diff --git a/tests/shared/Fixtures/approved/request-product-identifier.editor.approved.txt b/tests/shared/Fixtures/approved/request-product-identifier.editor.approved.txt new file mode 100644 index 0000000..02da221 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-identifier.editor.approved.txt @@ -0,0 +1,5 @@ +{ + "adapty_product_id": "adapty-product-1", + "base_plan_id": "monthly-base-plan", + "vendor_product_id": "com.adapty.sample.monthly" +} diff --git a/tests/shared/Fixtures/approved/request-product-identifier.ios.approved.txt b/tests/shared/Fixtures/approved/request-product-identifier.ios.approved.txt new file mode 100644 index 0000000..02da221 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-identifier.ios.approved.txt @@ -0,0 +1,5 @@ +{ + "adapty_product_id": "adapty-product-1", + "base_plan_id": "monthly-base-plan", + "vendor_product_id": "com.adapty.sample.monthly" +} diff --git a/tests/shared/Fixtures/approved/request-product-plain.android.approved.txt b/tests/shared/Fixtures/approved/request-product-plain.android.approved.txt new file mode 100644 index 0000000..57ddf50 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-plain.android.approved.txt @@ -0,0 +1,10 @@ +{ + "access_level_id": "premium", + "adapty_product_id": "adapty-product-2", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "paywall_product_index": 1, + "paywall_variation_id": "variation-0001", + "product_type": "non_consumable", + "vendor_product_id": "com.adapty.sample.lifetime" +} diff --git a/tests/shared/Fixtures/approved/request-product-plain.editor.approved.txt b/tests/shared/Fixtures/approved/request-product-plain.editor.approved.txt new file mode 100644 index 0000000..57ddf50 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-plain.editor.approved.txt @@ -0,0 +1,10 @@ +{ + "access_level_id": "premium", + "adapty_product_id": "adapty-product-2", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "paywall_product_index": 1, + "paywall_variation_id": "variation-0001", + "product_type": "non_consumable", + "vendor_product_id": "com.adapty.sample.lifetime" +} diff --git a/tests/shared/Fixtures/approved/request-product-plain.ios.approved.txt b/tests/shared/Fixtures/approved/request-product-plain.ios.approved.txt new file mode 100644 index 0000000..57ddf50 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-plain.ios.approved.txt @@ -0,0 +1,10 @@ +{ + "access_level_id": "premium", + "adapty_product_id": "adapty-product-2", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "paywall_product_index": 1, + "paywall_variation_id": "variation-0001", + "product_type": "non_consumable", + "vendor_product_id": "com.adapty.sample.lifetime" +} diff --git a/tests/shared/Fixtures/approved/request-product-with-offer.android.approved.txt b/tests/shared/Fixtures/approved/request-product-with-offer.android.approved.txt new file mode 100644 index 0000000..16cf7d2 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-with-offer.android.approved.txt @@ -0,0 +1,16 @@ +{ + "access_level_id": "premium", + "adapty_product_id": "adapty-product-1", + "payload_data": "{\"custom\":\"product payload\"}", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "paywall_product_index": 0, + "paywall_variation_id": "variation-0001", + "product_type": "subscription", + "subscription_offer_identifier": { + "id": "intro-1", + "type": "introductory" + }, + "vendor_product_id": "com.adapty.sample.monthly", + "web_purchase_url": "https://pay.adapty.io/checkout/abc" +} diff --git a/tests/shared/Fixtures/approved/request-product-with-offer.editor.approved.txt b/tests/shared/Fixtures/approved/request-product-with-offer.editor.approved.txt new file mode 100644 index 0000000..16cf7d2 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-with-offer.editor.approved.txt @@ -0,0 +1,16 @@ +{ + "access_level_id": "premium", + "adapty_product_id": "adapty-product-1", + "payload_data": "{\"custom\":\"product payload\"}", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "paywall_product_index": 0, + "paywall_variation_id": "variation-0001", + "product_type": "subscription", + "subscription_offer_identifier": { + "id": "intro-1", + "type": "introductory" + }, + "vendor_product_id": "com.adapty.sample.monthly", + "web_purchase_url": "https://pay.adapty.io/checkout/abc" +} diff --git a/tests/shared/Fixtures/approved/request-product-with-offer.ios.approved.txt b/tests/shared/Fixtures/approved/request-product-with-offer.ios.approved.txt new file mode 100644 index 0000000..16cf7d2 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-product-with-offer.ios.approved.txt @@ -0,0 +1,16 @@ +{ + "access_level_id": "premium", + "adapty_product_id": "adapty-product-1", + "payload_data": "{\"custom\":\"product payload\"}", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "paywall_product_index": 0, + "paywall_variation_id": "variation-0001", + "product_type": "subscription", + "subscription_offer_identifier": { + "id": "intro-1", + "type": "introductory" + }, + "vendor_product_id": "com.adapty.sample.monthly", + "web_purchase_url": "https://pay.adapty.io/checkout/abc" +} diff --git a/tests/shared/Fixtures/approved/request-profile-parameters.android.approved.txt b/tests/shared/Fixtures/approved/request-profile-parameters.android.approved.txt new file mode 100644 index 0000000..2db9c62 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-profile-parameters.android.approved.txt @@ -0,0 +1,13 @@ +{ + "analytics_disabled": false, + "birthday": "1815-12-10", + "custom_attributes": { + "plan": "gold", + "score": 12.5 + }, + "email": "ada@example.com", + "first_name": "Ada", + "gender": "f", + "last_name": "Lovelace", + "phone_number": "+15550100" +} diff --git a/tests/shared/Fixtures/approved/request-profile-parameters.editor.approved.txt b/tests/shared/Fixtures/approved/request-profile-parameters.editor.approved.txt new file mode 100644 index 0000000..2db9c62 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-profile-parameters.editor.approved.txt @@ -0,0 +1,13 @@ +{ + "analytics_disabled": false, + "birthday": "1815-12-10", + "custom_attributes": { + "plan": "gold", + "score": 12.5 + }, + "email": "ada@example.com", + "first_name": "Ada", + "gender": "f", + "last_name": "Lovelace", + "phone_number": "+15550100" +} diff --git a/tests/shared/Fixtures/approved/request-profile-parameters.ios.approved.txt b/tests/shared/Fixtures/approved/request-profile-parameters.ios.approved.txt new file mode 100644 index 0000000..2db9c62 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-profile-parameters.ios.approved.txt @@ -0,0 +1,13 @@ +{ + "analytics_disabled": false, + "birthday": "1815-12-10", + "custom_attributes": { + "plan": "gold", + "score": 12.5 + }, + "email": "ada@example.com", + "first_name": "Ada", + "gender": "f", + "last_name": "Lovelace", + "phone_number": "+15550100" +} diff --git a/tests/shared/Fixtures/approved/request-purchase-parameters-empty.android.approved.txt b/tests/shared/Fixtures/approved/request-purchase-parameters-empty.android.approved.txt new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-purchase-parameters-empty.android.approved.txt @@ -0,0 +1 @@ +{} diff --git a/tests/shared/Fixtures/approved/request-purchase-parameters-empty.editor.approved.txt b/tests/shared/Fixtures/approved/request-purchase-parameters-empty.editor.approved.txt new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-purchase-parameters-empty.editor.approved.txt @@ -0,0 +1 @@ +{} diff --git a/tests/shared/Fixtures/approved/request-purchase-parameters-empty.ios.approved.txt b/tests/shared/Fixtures/approved/request-purchase-parameters-empty.ios.approved.txt new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-purchase-parameters-empty.ios.approved.txt @@ -0,0 +1 @@ +{} diff --git a/tests/shared/Fixtures/approved/request-purchase-parameters-full.android.approved.txt b/tests/shared/Fixtures/approved/request-purchase-parameters-full.android.approved.txt new file mode 100644 index 0000000..f8a0bff --- /dev/null +++ b/tests/shared/Fixtures/approved/request-purchase-parameters-full.android.approved.txt @@ -0,0 +1,7 @@ +{ + "is_offer_personalized": true, + "subscription_update_params": { + "old_sub_vendor_product_id": "com.adapty.sample.monthly", + "replacement_mode": "deferred" + } +} diff --git a/tests/shared/Fixtures/approved/request-purchase-parameters-full.editor.approved.txt b/tests/shared/Fixtures/approved/request-purchase-parameters-full.editor.approved.txt new file mode 100644 index 0000000..f8a0bff --- /dev/null +++ b/tests/shared/Fixtures/approved/request-purchase-parameters-full.editor.approved.txt @@ -0,0 +1,7 @@ +{ + "is_offer_personalized": true, + "subscription_update_params": { + "old_sub_vendor_product_id": "com.adapty.sample.monthly", + "replacement_mode": "deferred" + } +} diff --git a/tests/shared/Fixtures/approved/request-purchase-parameters-full.ios.approved.txt b/tests/shared/Fixtures/approved/request-purchase-parameters-full.ios.approved.txt new file mode 100644 index 0000000..f8a0bff --- /dev/null +++ b/tests/shared/Fixtures/approved/request-purchase-parameters-full.ios.approved.txt @@ -0,0 +1,7 @@ +{ + "is_offer_personalized": true, + "subscription_update_params": { + "old_sub_vendor_product_id": "com.adapty.sample.monthly", + "replacement_mode": "deferred" + } +} diff --git a/tests/shared/Fixtures/approved/request-subscription-update.android.approved.txt b/tests/shared/Fixtures/approved/request-subscription-update.android.approved.txt new file mode 100644 index 0000000..9ac3f29 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-subscription-update.android.approved.txt @@ -0,0 +1,4 @@ +{ + "old_sub_vendor_product_id": "com.adapty.sample.monthly", + "replacement_mode": "charge_prorated_price" +} diff --git a/tests/shared/Fixtures/approved/request-subscription-update.editor.approved.txt b/tests/shared/Fixtures/approved/request-subscription-update.editor.approved.txt new file mode 100644 index 0000000..9ac3f29 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-subscription-update.editor.approved.txt @@ -0,0 +1,4 @@ +{ + "old_sub_vendor_product_id": "com.adapty.sample.monthly", + "replacement_mode": "charge_prorated_price" +} diff --git a/tests/shared/Fixtures/approved/request-subscription-update.ios.approved.txt b/tests/shared/Fixtures/approved/request-subscription-update.ios.approved.txt new file mode 100644 index 0000000..9ac3f29 --- /dev/null +++ b/tests/shared/Fixtures/approved/request-subscription-update.ios.approved.txt @@ -0,0 +1,4 @@ +{ + "old_sub_vendor_product_id": "com.adapty.sample.monthly", + "replacement_mode": "charge_prorated_price" +} diff --git a/tests/shared/Fixtures/approved/transport-activate.editor.approved.txt b/tests/shared/Fixtures/approved/transport-activate.editor.approved.txt new file mode 100644 index 0000000..23ab469 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-activate.editor.approved.txt @@ -0,0 +1,27 @@ +{ + "configuration": { + "activate_ui": false, + "api_key": "public_live_key", + "apple_idfa_collection_disabled": false, + "backend_proxy_host": "proxy.example.com", + "backend_proxy_port": 8080, + "cross_platform_sdk_name": "unity", + "cross_platform_sdk_version": "", + "customer_identity_parameters": { + "obfuscated_account_id": "obfuscated-1" + }, + "customer_user_id": "user-1", + "google_adid_collection_disabled": false, + "google_enable_pending_prepaid_plans": false, + "ip_address_collection_disabled": true, + "log_level": "error", + "media_cache": { + "disk_storage_size_limit": 300, + "memory_storage_count_limit": 200, + "memory_storage_total_cost_limit": 100 + }, + "observer_mode": true, + "server_cluster": "eu" + }, + "method": "activate" +} diff --git a/tests/shared/Fixtures/approved/transport-create-flow-view.editor.approved.txt b/tests/shared/Fixtures/approved/transport-create-flow-view.editor.approved.txt new file mode 100644 index 0000000..67823ea --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-create-flow-view.editor.approved.txt @@ -0,0 +1,85 @@ +{ + "custom_assets": [ + { + "id": "hero_data", + "type": "image", + "value": "AQID+g==" + }, + { + "asset_id": "hero-asset-id", + "id": "hero_asset", + "type": "image" + }, + { + "id": "hero_file", + "path": "images/hero.png", + "type": "image" + }, + { + "asset_id": "clip-asset-id", + "id": "clip_asset", + "type": "video" + }, + { + "id": "clip_file", + "path": "videos/clip.mp4", + "type": "video" + }, + { + "id": "accent", + "type": "color", + "value": "#336699FF" + }, + { + "id": "backdrop", + "points": { + "x0": 0, + "x1": 1, + "y0": 0, + "y1": 0 + }, + "type": "linear-gradient", + "values": [ + { + "color": "#FF0000FF", + "p": 0 + }, + { + "color": "#BF004080", + "p": 0.25 + }, + { + "color": "#0000FF00", + "p": 1 + } + ] + } + ], + "custom_tags": { + "NAME": "Ada" + }, + "custom_timers": { + "OFFER_END": "2026-07-30T10:00:00.000Z" + }, + "enable_safe_area_paddings": false, + "flow": { + "flow_id": "flow-0002", + "flow_name": "Minimal Flow", + "placement": { + "ab_test_name": "default", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0002", + "revision": 1 + }, + "remote_configs": [], + "response_created_at": 1753876800000, + "variation_id": "variation-0002", + "variations": [] + }, + "load_timeout": 12, + "locale": "es", + "method": "adapty_ui_create_flow_view", + "preload_products": true +} diff --git a/tests/shared/Fixtures/approved/transport-create-web-paywall-url-paywall.editor.approved.txt b/tests/shared/Fixtures/approved/transport-create-web-paywall-url-paywall.editor.approved.txt new file mode 100644 index 0000000..f459169 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-create-web-paywall-url-paywall.editor.approved.txt @@ -0,0 +1,32 @@ +{ + "method": "create_web_paywall_url", + "paywall": { + "paywall_id": "paywall-0001", + "paywall_name": "Winter Paywall", + "placement": { + "ab_test_name": "winter_test", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": true, + "placement_audience_version_id": "pav-0001", + "revision": 7 + }, + "products": [ + { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-1", + "flow_product_id": "flow-product-1", + "product_type": "subscription", + "vendor_product_id": "com.adapty.sample.monthly" + }, + { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-2", + "product_type": "subscription", + "vendor_product_id": "com.adapty.sample.yearly" + } + ], + "variation_id": "variation-0001", + "web_purchase_url": "https://pay.adapty.io/checkout/abc" + } +} diff --git a/tests/shared/Fixtures/approved/transport-create-web-paywall-url-product.editor.approved.txt b/tests/shared/Fixtures/approved/transport-create-web-paywall-url-product.editor.approved.txt new file mode 100644 index 0000000..32d2a06 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-create-web-paywall-url-product.editor.approved.txt @@ -0,0 +1,19 @@ +{ + "method": "create_web_paywall_url", + "product": { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-1", + "payload_data": "{\"custom\":\"product payload\"}", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "paywall_product_index": 0, + "paywall_variation_id": "variation-0001", + "product_type": "subscription", + "subscription_offer_identifier": { + "id": "intro-1", + "type": "introductory" + }, + "vendor_product_id": "com.adapty.sample.monthly", + "web_purchase_url": "https://pay.adapty.io/checkout/abc" + } +} diff --git a/tests/shared/Fixtures/approved/transport-dismiss-flow-view.editor.approved.txt b/tests/shared/Fixtures/approved/transport-dismiss-flow-view.editor.approved.txt new file mode 100644 index 0000000..ee0fa88 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-dismiss-flow-view.editor.approved.txt @@ -0,0 +1,5 @@ +{ + "destroy": true, + "id": "view-1", + "method": "adapty_ui_dismiss_flow_view" +} diff --git a/tests/shared/Fixtures/approved/transport-get-flow.editor.approved.txt b/tests/shared/Fixtures/approved/transport-get-flow.editor.approved.txt new file mode 100644 index 0000000..885942d --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-get-flow.editor.approved.txt @@ -0,0 +1,9 @@ +{ + "fetch_policy": { + "max_age": 90, + "type": "return_cache_data_if_not_expired_else_load" + }, + "load_timeout": 5, + "method": "get_flow", + "placement_id": "onboarding" +} diff --git a/tests/shared/Fixtures/approved/transport-get-paywall-products.editor.approved.txt b/tests/shared/Fixtures/approved/transport-get-paywall-products.editor.approved.txt new file mode 100644 index 0000000..10289f1 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-get-paywall-products.editor.approved.txt @@ -0,0 +1,19 @@ +{ + "flow": { + "flow_id": "flow-0002", + "flow_name": "Minimal Flow", + "placement": { + "ab_test_name": "default", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0002", + "revision": 1 + }, + "remote_configs": [], + "response_created_at": 1753876800000, + "variation_id": "variation-0002", + "variations": [] + }, + "method": "get_paywall_products" +} diff --git a/tests/shared/Fixtures/approved/transport-get-profile.editor.approved.txt b/tests/shared/Fixtures/approved/transport-get-profile.editor.approved.txt new file mode 100644 index 0000000..8285827 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-get-profile.editor.approved.txt @@ -0,0 +1,3 @@ +{ + "method": "get_profile" +} diff --git a/tests/shared/Fixtures/approved/transport-identify.editor.approved.txt b/tests/shared/Fixtures/approved/transport-identify.editor.approved.txt new file mode 100644 index 0000000..c7f3aab --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-identify.editor.approved.txt @@ -0,0 +1,7 @@ +{ + "customer_user_id": "user-1", + "method": "identify", + "parameters": { + "obfuscated_account_id": "obfuscated-1" + } +} diff --git a/tests/shared/Fixtures/approved/transport-log-show-flow.editor.approved.txt b/tests/shared/Fixtures/approved/transport-log-show-flow.editor.approved.txt new file mode 100644 index 0000000..754bf54 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-log-show-flow.editor.approved.txt @@ -0,0 +1,19 @@ +{ + "flow": { + "flow_id": "flow-0002", + "flow_name": "Minimal Flow", + "placement": { + "ab_test_name": "default", + "audience_name": "All Users", + "developer_id": "onboarding", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0002", + "revision": 1 + }, + "remote_configs": [], + "response_created_at": 1753876800000, + "variation_id": "variation-0002", + "variations": [] + }, + "method": "log_show_flow" +} diff --git a/tests/shared/Fixtures/approved/transport-logout.editor.approved.txt b/tests/shared/Fixtures/approved/transport-logout.editor.approved.txt new file mode 100644 index 0000000..4b100cb --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-logout.editor.approved.txt @@ -0,0 +1,3 @@ +{ + "method": "logout" +} diff --git a/tests/shared/Fixtures/approved/transport-make-purchase-with-offer.editor.approved.txt b/tests/shared/Fixtures/approved/transport-make-purchase-with-offer.editor.approved.txt new file mode 100644 index 0000000..f85f885 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-make-purchase-with-offer.editor.approved.txt @@ -0,0 +1,19 @@ +{ + "method": "make_purchase", + "product": { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-1", + "payload_data": "{\"custom\":\"product payload\"}", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "paywall_product_index": 0, + "paywall_variation_id": "variation-0001", + "product_type": "subscription", + "subscription_offer_identifier": { + "id": "intro-1", + "type": "introductory" + }, + "vendor_product_id": "com.adapty.sample.monthly", + "web_purchase_url": "https://pay.adapty.io/checkout/abc" + } +} diff --git a/tests/shared/Fixtures/approved/transport-make-purchase-without-offer.editor.approved.txt b/tests/shared/Fixtures/approved/transport-make-purchase-without-offer.editor.approved.txt new file mode 100644 index 0000000..c217771 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-make-purchase-without-offer.editor.approved.txt @@ -0,0 +1,13 @@ +{ + "method": "make_purchase", + "product": { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-2", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "paywall_product_index": 1, + "paywall_variation_id": "variation-0001", + "product_type": "non_consumable", + "vendor_product_id": "com.adapty.sample.lifetime" + } +} diff --git a/tests/shared/Fixtures/approved/transport-open-url.editor.approved.txt b/tests/shared/Fixtures/approved/transport-open-url.editor.approved.txt new file mode 100644 index 0000000..c4697ba --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-open-url.editor.approved.txt @@ -0,0 +1,5 @@ +{ + "method": "adapty_ui_open_url", + "open_in": "browser_out_app", + "url": "https://adapty.io" +} diff --git a/tests/shared/Fixtures/approved/transport-open-web-paywall-product.editor.approved.txt b/tests/shared/Fixtures/approved/transport-open-web-paywall-product.editor.approved.txt new file mode 100644 index 0000000..963503f --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-open-web-paywall-product.editor.approved.txt @@ -0,0 +1,20 @@ +{ + "method": "open_web_paywall", + "open_in": "browser_in_app", + "product": { + "access_level_id": "premium", + "adapty_product_id": "adapty-product-1", + "payload_data": "{\"custom\":\"product payload\"}", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "paywall_product_index": 0, + "paywall_variation_id": "variation-0001", + "product_type": "subscription", + "subscription_offer_identifier": { + "id": "intro-1", + "type": "introductory" + }, + "vendor_product_id": "com.adapty.sample.monthly", + "web_purchase_url": "https://pay.adapty.io/checkout/abc" + } +} diff --git a/tests/shared/Fixtures/approved/transport-present-code-redemption-sheet.editor.approved.txt b/tests/shared/Fixtures/approved/transport-present-code-redemption-sheet.editor.approved.txt new file mode 100644 index 0000000..f38380b --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-present-code-redemption-sheet.editor.approved.txt @@ -0,0 +1,3 @@ +{ + "method": "present_code_redemption_sheet" +} diff --git a/tests/shared/Fixtures/approved/transport-present-flow-view.editor.approved.txt b/tests/shared/Fixtures/approved/transport-present-flow-view.editor.approved.txt new file mode 100644 index 0000000..b65d78f --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-present-flow-view.editor.approved.txt @@ -0,0 +1,5 @@ +{ + "id": "view-1", + "ios_presentation_style": "full_screen", + "method": "adapty_ui_present_flow_view" +} diff --git a/tests/shared/Fixtures/approved/transport-report-transaction.editor.approved.txt b/tests/shared/Fixtures/approved/transport-report-transaction.editor.approved.txt new file mode 100644 index 0000000..3b7baa2 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-report-transaction.editor.approved.txt @@ -0,0 +1,5 @@ +{ + "method": "report_transaction", + "transaction_id": "txn-1", + "variation_id": "variation-0001" +} diff --git a/tests/shared/Fixtures/approved/transport-restore-purchases.editor.approved.txt b/tests/shared/Fixtures/approved/transport-restore-purchases.editor.approved.txt new file mode 100644 index 0000000..f9f051d --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-restore-purchases.editor.approved.txt @@ -0,0 +1,3 @@ +{ + "method": "restore_purchases" +} diff --git a/tests/shared/Fixtures/approved/transport-set-fallback.editor.approved.txt b/tests/shared/Fixtures/approved/transport-set-fallback.editor.approved.txt new file mode 100644 index 0000000..6c5bc14 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-set-fallback.editor.approved.txt @@ -0,0 +1,3 @@ +{ + "method": "set_fallback" +} diff --git a/tests/shared/Fixtures/approved/transport-set-integration-identifier.editor.approved.txt b/tests/shared/Fixtures/approved/transport-set-integration-identifier.editor.approved.txt new file mode 100644 index 0000000..1ff97bc --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-set-integration-identifier.editor.approved.txt @@ -0,0 +1,6 @@ +{ + "key_values": { + "appsflyer_id": "af-1" + }, + "method": "set_integration_identifiers" +} diff --git a/tests/shared/Fixtures/approved/transport-set-log-level.editor.approved.txt b/tests/shared/Fixtures/approved/transport-set-log-level.editor.approved.txt new file mode 100644 index 0000000..afb50a9 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-set-log-level.editor.approved.txt @@ -0,0 +1,4 @@ +{ + "method": "set_log_level", + "value": "verbose" +} diff --git a/tests/shared/Fixtures/approved/transport-show-dialog.editor.approved.txt b/tests/shared/Fixtures/approved/transport-show-dialog.editor.approved.txt new file mode 100644 index 0000000..90bdbc6 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-show-dialog.editor.approved.txt @@ -0,0 +1,10 @@ +{ + "configuration": { + "content": "You keep access until the end of the period.", + "default_action_title": "Keep", + "secondary_action_title": "Cancel", + "title": "Cancel subscription?" + }, + "id": "view-1", + "method": "adapty_ui_show_dialog" +} diff --git a/tests/shared/Fixtures/approved/transport-update-attribution.editor.approved.txt b/tests/shared/Fixtures/approved/transport-update-attribution.editor.approved.txt new file mode 100644 index 0000000..cdf1aaf --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-update-attribution.editor.approved.txt @@ -0,0 +1,5 @@ +{ + "attribution": "{\"status\":\"organic\",\"clicks\":3,\"cost\":1.5,\"is_retargeting\":false,\"install_time\":\"2026-07-30T10:00:00.000Z\",\"campaign\":null,\"tags\":[\"a\",\"b\"],\"nested\":{\"k\":\"v\"}}", + "method": "update_attribution_data", + "source": "appsflyer" +} diff --git a/tests/shared/Fixtures/approved/transport-update-collecting-refund-data-consent.editor.approved.txt b/tests/shared/Fixtures/approved/transport-update-collecting-refund-data-consent.editor.approved.txt new file mode 100644 index 0000000..e79a3ac --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-update-collecting-refund-data-consent.editor.approved.txt @@ -0,0 +1,4 @@ +{ + "consent": true, + "method": "update_collecting_refund_data_consent" +} diff --git a/tests/shared/Fixtures/approved/transport-update-profile.editor.approved.txt b/tests/shared/Fixtures/approved/transport-update-profile.editor.approved.txt new file mode 100644 index 0000000..d4c95c6 --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-update-profile.editor.approved.txt @@ -0,0 +1,16 @@ +{ + "method": "update_profile", + "params": { + "analytics_disabled": false, + "birthday": "1815-12-10", + "custom_attributes": { + "plan": "gold", + "score": 12.5 + }, + "email": "ada@example.com", + "first_name": "Ada", + "gender": "f", + "last_name": "Lovelace", + "phone_number": "+15550100" + } +} diff --git a/tests/shared/Fixtures/approved/transport-update-refund-preference.editor.approved.txt b/tests/shared/Fixtures/approved/transport-update-refund-preference.editor.approved.txt new file mode 100644 index 0000000..264268d --- /dev/null +++ b/tests/shared/Fixtures/approved/transport-update-refund-preference.editor.approved.txt @@ -0,0 +1,4 @@ +{ + "method": "update_refund_preference", + "refund_preference": "grant" +} diff --git a/tests/shared/Fixtures/approved/user-action-full.android.approved.txt b/tests/shared/Fixtures/approved/user-action-full.android.approved.txt new file mode 100644 index 0000000..d0d6d9e --- /dev/null +++ b/tests/shared/Fixtures/approved/user-action-full.android.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyUIUserAction", + "OpenIn": "ExternalBrowser (0)", + "Type": "OpenUrl (2)", + "Value": "https://adapty.io/terms" +} diff --git a/tests/shared/Fixtures/approved/user-action-full.editor.approved.txt b/tests/shared/Fixtures/approved/user-action-full.editor.approved.txt new file mode 100644 index 0000000..d0d6d9e --- /dev/null +++ b/tests/shared/Fixtures/approved/user-action-full.editor.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyUIUserAction", + "OpenIn": "ExternalBrowser (0)", + "Type": "OpenUrl (2)", + "Value": "https://adapty.io/terms" +} diff --git a/tests/shared/Fixtures/approved/user-action-full.ios.approved.txt b/tests/shared/Fixtures/approved/user-action-full.ios.approved.txt new file mode 100644 index 0000000..d0d6d9e --- /dev/null +++ b/tests/shared/Fixtures/approved/user-action-full.ios.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyUIUserAction", + "OpenIn": "ExternalBrowser (0)", + "Type": "OpenUrl (2)", + "Value": "https://adapty.io/terms" +} diff --git a/tests/shared/Fixtures/approved/user-action-minimal.android.approved.txt b/tests/shared/Fixtures/approved/user-action-minimal.android.approved.txt new file mode 100644 index 0000000..fa3ba0b --- /dev/null +++ b/tests/shared/Fixtures/approved/user-action-minimal.android.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyUIUserAction", + "OpenIn": null, + "Type": "Close (0)", + "Value": null +} diff --git a/tests/shared/Fixtures/approved/user-action-minimal.editor.approved.txt b/tests/shared/Fixtures/approved/user-action-minimal.editor.approved.txt new file mode 100644 index 0000000..fa3ba0b --- /dev/null +++ b/tests/shared/Fixtures/approved/user-action-minimal.editor.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyUIUserAction", + "OpenIn": null, + "Type": "Close (0)", + "Value": null +} diff --git a/tests/shared/Fixtures/approved/user-action-minimal.ios.approved.txt b/tests/shared/Fixtures/approved/user-action-minimal.ios.approved.txt new file mode 100644 index 0000000..fa3ba0b --- /dev/null +++ b/tests/shared/Fixtures/approved/user-action-minimal.ios.approved.txt @@ -0,0 +1,6 @@ +{ + "$type": "AdaptyUIUserAction", + "OpenIn": null, + "Type": "Close (0)", + "Value": null +} diff --git a/tests/shared/Fixtures/pbxproj/edm-package-reference.applied.pbxproj b/tests/shared/Fixtures/pbxproj/edm-package-reference.applied.pbxproj new file mode 100644 index 0000000..fa2f589 --- /dev/null +++ b/tests/shared/Fixtures/pbxproj/edm-package-reference.applied.pbxproj @@ -0,0 +1,92 @@ +/* Begin XCConfigurationList section */ + 1D6058960D05DD3E006BFB54 /* Build configuration list for PBXNativeTarget "Unity-iPhone" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1D6058950D05DD3E006BFB54 /* Release */, + 56E860841D67581C00A1AB2B /* ReleaseForProfiling */, + 56E860811D6757FF00A1AB2B /* ReleaseForRunning */, + 1D6058940D05DD3E006BFB54 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5623C58517FDCB0900090B9E /* Build configuration list for PBXNativeTarget "Unity-iPhone Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5623C58317FDCB0900090B9E /* Release */, + 56E860851D67581C00A1AB2B /* ReleaseForProfiling */, + 56E860821D6757FF00A1AB2B /* ReleaseForRunning */, + 5623C58417FDCB0900090B9E /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7F4E05AA2717216D00A2CBE4 /* Build configuration list for PBXNativeTarget "GameAssembly" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7F4E05A62717216D00A2CBE4 /* Release */, + 7F4E05A72717216D00A2CBE4 /* ReleaseForProfiling */, + 7F4E05A82717216D00A2CBE4 /* ReleaseForRunning */, + 7F4E05A92717216D00A2CBE4 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9D25ABA6213FB47800354C27 /* Build configuration list for PBXNativeTarget "UnityFramework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9D25ABA7213FB47800354C27 /* Release */, + 9D25ABA8213FB47800354C27 /* ReleaseForProfiling */, + 9D25ABA9213FB47800354C27 /* ReleaseForRunning */, + 9D25ABAA213FB47800354C27 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Unity-iPhone" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF5008A954540054247B /* Release */, + 56E860831D67581C00A1AB2B /* ReleaseForProfiling */, + 56E860801D6757FF00A1AB2B /* ReleaseForRunning */, + C01FCF4F08A954540054247B /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 39F241ABA0403F96E3BD5E5F /* XCRemoteSwiftPackageReference "AdaptySDK-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/adaptyteam/AdaptySDK-iOS.git"; + requirement = { + kind = exactVersion; + version = 4.0.2; + }; + traits = ( + KidsMode, + ); + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 8F994383B1395C3F76F2D440 /* AdaptyUI */ = { + isa = XCSwiftPackageProductDependency; + package = 39F241ABA0403F96E3BD5E5F /* XCRemoteSwiftPackageReference "AdaptySDK-iOS" */; + productName = AdaptyUI; + }; + D374472FA256C376C813975B /* Adapty */ = { + isa = XCSwiftPackageProductDependency; + package = 39F241ABA0403F96E3BD5E5F /* XCRemoteSwiftPackageReference "AdaptySDK-iOS" */; + productName = Adapty; + }; + DFDA4E14B677C8D03540E716 /* AdaptyPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 39F241ABA0403F96E3BD5E5F /* XCRemoteSwiftPackageReference "AdaptySDK-iOS" */; + productName = AdaptyPlugin; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; +} \ No newline at end of file diff --git a/tests/shared/Fixtures/pbxproj/edm-package-reference.pbxproj b/tests/shared/Fixtures/pbxproj/edm-package-reference.pbxproj new file mode 100644 index 0000000..4439857 --- /dev/null +++ b/tests/shared/Fixtures/pbxproj/edm-package-reference.pbxproj @@ -0,0 +1,89 @@ +/* Begin XCConfigurationList section */ + 1D6058960D05DD3E006BFB54 /* Build configuration list for PBXNativeTarget "Unity-iPhone" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1D6058950D05DD3E006BFB54 /* Release */, + 56E860841D67581C00A1AB2B /* ReleaseForProfiling */, + 56E860811D6757FF00A1AB2B /* ReleaseForRunning */, + 1D6058940D05DD3E006BFB54 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5623C58517FDCB0900090B9E /* Build configuration list for PBXNativeTarget "Unity-iPhone Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5623C58317FDCB0900090B9E /* Release */, + 56E860851D67581C00A1AB2B /* ReleaseForProfiling */, + 56E860821D6757FF00A1AB2B /* ReleaseForRunning */, + 5623C58417FDCB0900090B9E /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7F4E05AA2717216D00A2CBE4 /* Build configuration list for PBXNativeTarget "GameAssembly" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7F4E05A62717216D00A2CBE4 /* Release */, + 7F4E05A72717216D00A2CBE4 /* ReleaseForProfiling */, + 7F4E05A82717216D00A2CBE4 /* ReleaseForRunning */, + 7F4E05A92717216D00A2CBE4 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9D25ABA6213FB47800354C27 /* Build configuration list for PBXNativeTarget "UnityFramework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9D25ABA7213FB47800354C27 /* Release */, + 9D25ABA8213FB47800354C27 /* ReleaseForProfiling */, + 9D25ABA9213FB47800354C27 /* ReleaseForRunning */, + 9D25ABAA213FB47800354C27 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Unity-iPhone" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF5008A954540054247B /* Release */, + 56E860831D67581C00A1AB2B /* ReleaseForProfiling */, + 56E860801D6757FF00A1AB2B /* ReleaseForRunning */, + C01FCF4F08A954540054247B /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 39F241ABA0403F96E3BD5E5F /* XCRemoteSwiftPackageReference "AdaptySDK-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/adaptyteam/AdaptySDK-iOS.git"; + requirement = { + kind = exactVersion; + version = 4.0.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 8F994383B1395C3F76F2D440 /* AdaptyUI */ = { + isa = XCSwiftPackageProductDependency; + package = 39F241ABA0403F96E3BD5E5F /* XCRemoteSwiftPackageReference "AdaptySDK-iOS" */; + productName = AdaptyUI; + }; + D374472FA256C376C813975B /* Adapty */ = { + isa = XCSwiftPackageProductDependency; + package = 39F241ABA0403F96E3BD5E5F /* XCRemoteSwiftPackageReference "AdaptySDK-iOS" */; + productName = Adapty; + }; + DFDA4E14B677C8D03540E716 /* AdaptyPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 39F241ABA0403F96E3BD5E5F /* XCRemoteSwiftPackageReference "AdaptySDK-iOS" */; + productName = AdaptyPlugin; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; +} \ No newline at end of file diff --git a/tests/shared/Fixtures/responses/error-full.json b/tests/shared/Fixtures/responses/error-full.json new file mode 100644 index 0000000..9da11fe --- /dev/null +++ b/tests/shared/Fixtures/responses/error-full.json @@ -0,0 +1,7 @@ +{ + "error": { + "adapty_code": 2003, + "message": "The product was not found", + "detail": "AdaptyError.productNotFound(vendorProductId: \"com.adapty.sample.monthly\")" + } +} diff --git a/tests/shared/Fixtures/responses/error-minimal.json b/tests/shared/Fixtures/responses/error-minimal.json new file mode 100644 index 0000000..9d95abe --- /dev/null +++ b/tests/shared/Fixtures/responses/error-minimal.json @@ -0,0 +1,6 @@ +{ + "error": { + "adapty_code": 0, + "message": "Unknown error" + } +} diff --git a/tests/shared/Fixtures/responses/flow-full.json b/tests/shared/Fixtures/responses/flow-full.json new file mode 100644 index 0000000..1b2e4fb --- /dev/null +++ b/tests/shared/Fixtures/responses/flow-full.json @@ -0,0 +1,61 @@ +{ + "placement": { + "developer_id": "onboarding", + "audience_name": "All Users", + "revision": 7, + "ab_test_name": "winter_test", + "is_tracking_purchases": true, + "placement_audience_version_id": "pav-0001" + }, + "flow_id": "flow-0001", + "flow_name": "Winter Flow", + "variation_id": "variation-0001", + "flow_version_id": "flow-version-0001", + "remote_configs": [ + { + "lang": "en", + "data": "{\"title\": \"Go premium\", \"discount\": 30, \"enabled\": true, \"nested\": {\"k\": \"v\"}, \"released_at\": \"2026-07-30T10:00:00.000Z\", \"tags\": [\"a\", \"b\"], \"mixed\": [1, true, null, {\"k\": \"v\"}], \"absent\": null}" + }, + { + "lang": "es", + "data": "{\"title\":\"Hazte premium\"}" + } + ], + "variations": [ + { + "placement": { + "developer_id": "onboarding", + "audience_name": "All Users", + "revision": 7, + "ab_test_name": "winter_test", + "is_tracking_purchases": true, + "placement_audience_version_id": "pav-0001" + }, + "paywall_id": "paywall-0001", + "paywall_name": "Winter Paywall", + "variation_id": "variation-0001", + "web_purchase_url": "https://pay.adapty.io/checkout/abc", + "products": [ + { + "flow_product_id": "flow-product-1", + "vendor_product_id": "com.adapty.sample.monthly", + "adapty_product_id": "adapty-product-1", + "access_level_id": "premium", + "product_type": "subscription", + "promotional_offer_id": "promo-1", + "win_back_offer_id": "winback-1", + "base_plan_id": "base-plan-1", + "offer_id": "offer-1" + }, + { + "vendor_product_id": "com.adapty.sample.yearly", + "adapty_product_id": "adapty-product-2", + "access_level_id": "premium", + "product_type": "subscription" + } + ] + } + ], + "payload_data": "{\"custom\":\"payload\"}", + "response_created_at": 1753876800000 +} diff --git a/tests/shared/Fixtures/responses/flow-minimal.json b/tests/shared/Fixtures/responses/flow-minimal.json new file mode 100644 index 0000000..4568fe3 --- /dev/null +++ b/tests/shared/Fixtures/responses/flow-minimal.json @@ -0,0 +1,14 @@ +{ + "placement": { + "developer_id": "onboarding", + "audience_name": "All Users", + "revision": 1, + "ab_test_name": "default", + "placement_audience_version_id": "pav-0002" + }, + "flow_id": "flow-0002", + "flow_name": "Minimal Flow", + "variation_id": "variation-0002", + "variations": [], + "response_created_at": 1753876800000 +} diff --git a/tests/shared/Fixtures/responses/installation-determined-minimal.json b/tests/shared/Fixtures/responses/installation-determined-minimal.json new file mode 100644 index 0000000..28728db --- /dev/null +++ b/tests/shared/Fixtures/responses/installation-determined-minimal.json @@ -0,0 +1,7 @@ +{ + "status": "determined", + "details": { + "install_time": "2026-07-30T10:00:00.000Z", + "app_launch_count": 1 + } +} diff --git a/tests/shared/Fixtures/responses/installation-determined.json b/tests/shared/Fixtures/responses/installation-determined.json new file mode 100644 index 0000000..94745fd --- /dev/null +++ b/tests/shared/Fixtures/responses/installation-determined.json @@ -0,0 +1,9 @@ +{ + "status": "determined", + "details": { + "install_id": "install-0001", + "install_time": "2026-07-30T10:00:00.000Z", + "app_launch_count": 7, + "payload": "{\"campaign\":\"summer\"}" + } +} diff --git a/tests/shared/Fixtures/responses/installation-not-available.json b/tests/shared/Fixtures/responses/installation-not-available.json new file mode 100644 index 0000000..b18c349 --- /dev/null +++ b/tests/shared/Fixtures/responses/installation-not-available.json @@ -0,0 +1 @@ +{ "status": "not_available" } diff --git a/tests/shared/Fixtures/responses/installation-not-determined.json b/tests/shared/Fixtures/responses/installation-not-determined.json new file mode 100644 index 0000000..dda452d --- /dev/null +++ b/tests/shared/Fixtures/responses/installation-not-determined.json @@ -0,0 +1 @@ +{ "status": "not_determined" } diff --git a/tests/shared/Fixtures/responses/onboarding-analytics-screen-completed-bare.json b/tests/shared/Fixtures/responses/onboarding-analytics-screen-completed-bare.json new file mode 100644 index 0000000..1f69084 --- /dev/null +++ b/tests/shared/Fixtures/responses/onboarding-analytics-screen-completed-bare.json @@ -0,0 +1,5 @@ +{ + "event": { + "name": "screen_completed" + } +} diff --git a/tests/shared/Fixtures/responses/onboarding-analytics-screen-completed.json b/tests/shared/Fixtures/responses/onboarding-analytics-screen-completed.json new file mode 100644 index 0000000..0fa3b78 --- /dev/null +++ b/tests/shared/Fixtures/responses/onboarding-analytics-screen-completed.json @@ -0,0 +1,7 @@ +{ + "event": { + "name": "screen_completed", + "element_id": "gender_select", + "reply": "female" + } +} diff --git a/tests/shared/Fixtures/responses/onboarding-analytics-started.json b/tests/shared/Fixtures/responses/onboarding-analytics-started.json new file mode 100644 index 0000000..19911b0 --- /dev/null +++ b/tests/shared/Fixtures/responses/onboarding-analytics-started.json @@ -0,0 +1,5 @@ +{ + "event": { + "name": "onboarding_started" + } +} diff --git a/tests/shared/Fixtures/responses/onboarding-analytics-unknown.json b/tests/shared/Fixtures/responses/onboarding-analytics-unknown.json new file mode 100644 index 0000000..a10193b --- /dev/null +++ b/tests/shared/Fixtures/responses/onboarding-analytics-unknown.json @@ -0,0 +1,5 @@ +{ + "event": { + "name": "paywall_screen_presented" + } +} diff --git a/tests/shared/Fixtures/responses/onboarding-date-picker-full.json b/tests/shared/Fixtures/responses/onboarding-date-picker-full.json new file mode 100644 index 0000000..230f88a --- /dev/null +++ b/tests/shared/Fixtures/responses/onboarding-date-picker-full.json @@ -0,0 +1,7 @@ +{ + "action": { + "element_id": "birthday", + "element_type": "date_picker", + "value": { "day": 30, "month": 7, "year": 2026 } + } +} diff --git a/tests/shared/Fixtures/responses/onboarding-date-picker-partial.json b/tests/shared/Fixtures/responses/onboarding-date-picker-partial.json new file mode 100644 index 0000000..e9e9367 --- /dev/null +++ b/tests/shared/Fixtures/responses/onboarding-date-picker-partial.json @@ -0,0 +1,7 @@ +{ + "action": { + "element_id": "birthday", + "element_type": "date_picker", + "value": { "year": 2026 } + } +} diff --git a/tests/shared/Fixtures/responses/onboarding-full.json b/tests/shared/Fixtures/responses/onboarding-full.json new file mode 100644 index 0000000..0778882 --- /dev/null +++ b/tests/shared/Fixtures/responses/onboarding-full.json @@ -0,0 +1,23 @@ +{ + "placement": { + "developer_id": "welcome", + "audience_name": "All Users", + "revision": 3, + "ab_test_name": "onboarding_test", + "is_tracking_purchases": false, + "placement_audience_version_id": "pav-0002" + }, + "onboarding_id": "onboarding-0001", + "onboarding_name": "Welcome Onboarding", + "variation_id": "variation-0002", + "remote_config": { + "lang": "en", + "data": "{\"steps\":3,\"skippable\":false}" + }, + "onboarding_builder": { + "config_url": "https://cdn.adapty.io/onboardings/onboarding-0001.json" + }, + "payload_data": "{\"custom\":\"onboarding payload\"}", + "response_created_at": 1754006400, + "request_locale": "en" +} diff --git a/tests/shared/Fixtures/responses/onboarding-minimal.json b/tests/shared/Fixtures/responses/onboarding-minimal.json new file mode 100644 index 0000000..44dfce2 --- /dev/null +++ b/tests/shared/Fixtures/responses/onboarding-minimal.json @@ -0,0 +1,17 @@ +{ + "placement": { + "developer_id": "welcome", + "audience_name": "All Users", + "revision": 1, + "ab_test_name": "onboarding_test", + "placement_audience_version_id": "pav-0003" + }, + "onboarding_id": "onboarding-0002", + "onboarding_name": "Short Onboarding", + "variation_id": "variation-0003", + "onboarding_builder": { + "config_url": "https://cdn.adapty.io/onboardings/onboarding-0002.json" + }, + "response_created_at": 1754006401, + "request_locale": "es" +} diff --git a/tests/shared/Fixtures/responses/onboarding-select.json b/tests/shared/Fixtures/responses/onboarding-select.json new file mode 100644 index 0000000..c90b4ab --- /dev/null +++ b/tests/shared/Fixtures/responses/onboarding-select.json @@ -0,0 +1,7 @@ +{ + "action": { + "element_id": "plan", + "element_type": "select", + "value": { "id": "plan-1", "value": "monthly", "label": "Monthly" } + } +} diff --git a/tests/shared/Fixtures/responses/products-full.json b/tests/shared/Fixtures/responses/products-full.json new file mode 100644 index 0000000..06d1c03 --- /dev/null +++ b/tests/shared/Fixtures/responses/products-full.json @@ -0,0 +1,70 @@ +[ + { + "vendor_product_id": "com.adapty.sample.monthly", + "flow_product_id": "flow-product-1", + "adapty_product_id": "adapty-product-1", + "access_level_id": "premium", + "product_type": "subscription", + "paywall_product_index": 0, + "paywall_variation_id": "variation-0001", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "web_purchase_url": "https://pay.adapty.io/checkout/abc", + "localized_description": "Full access, billed monthly", + "localized_title": "Monthly", + "is_family_shareable": true, + "region_code": "US", + "price": { + "amount": 9.99, + "currency_code": "USD", + "currency_symbol": "$", + "localized_string": "$9.99" + }, + "subscription": { + "group_identifier": "group-1", + "period": { "unit": "month", "number_of_units": 1 }, + "localized_period": "1 month", + "renewal_type": "prepaid", + "base_plan_id": "base-plan-1", + "offer": { + "offer_identifier": { "id": "intro-1", "type": "introductory" }, + "phases": [ + { + "price": { + "amount": 0.0, + "currency_code": "USD", + "currency_symbol": "$", + "localized_string": "Free" + }, + "number_of_periods": 1, + "payment_mode": "free_trial", + "subscription_period": { "unit": "week", "number_of_units": 1 }, + "localized_subscription_period": "1 week", + "localized_number_of_periods": "1 time" + } + ], + "offer_tags": ["tag-a", "tag-b"] + } + }, + "payload_data": "{\"custom\":\"product payload\"}" + }, + { + "vendor_product_id": "com.adapty.sample.lifetime", + "adapty_product_id": "adapty-product-2", + "access_level_id": "premium", + "product_type": "non_consumable", + "paywall_product_index": 1, + "paywall_variation_id": "variation-0001", + "paywall_ab_test_name": "winter_test", + "paywall_name": "Winter Paywall", + "localized_description": "One-time purchase", + "localized_title": "Lifetime", + "is_family_shareable": false, + "price": { + "amount": 99.0, + "currency_code": "USD", + "currency_symbol": "$", + "localized_string": "$99.00" + } + } +] diff --git a/tests/shared/Fixtures/responses/profile-full.json b/tests/shared/Fixtures/responses/profile-full.json new file mode 100644 index 0000000..357979c --- /dev/null +++ b/tests/shared/Fixtures/responses/profile-full.json @@ -0,0 +1,74 @@ +{ + "profile_id": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "customer_user_id": "user-42", + "segment_hash": "8f14e45fceea167a", + "custom_attributes": { + "favourite_colour": "green", + "score": 12.5 + }, + "paid_access_levels": { + "premium": { + "id": "premium", + "is_active": true, + "vendor_product_id": "com.adapty.sample.monthly", + "store": "app_store", + "activated_at": "2026-01-15T09:30:00.000Z", + "renewed_at": "2026-07-15T09:30:00.000Z", + "expires_at": "2026-08-15T09:30:00.000Z", + "starts_at": "2026-01-15T09:30:00.000Z", + "is_lifetime": false, + "active_introductory_offer_type": "free_trial", + "active_promotional_offer_type": "promotional", + "active_promotional_offer_id": "promo-winter", + "offer_id": "offer-1", + "will_renew": true, + "is_in_grace_period": false, + "unsubscribed_at": "2026-07-20T10:00:00.000Z", + "billing_issue_detected_at": "2026-07-19T10:00:00.000Z", + "cancellation_reason": "voluntarily_cancelled", + "is_refund": false + } + }, + "subscriptions": { + "com.adapty.sample.monthly": { + "store": "app_store", + "vendor_product_id": "com.adapty.sample.monthly", + "vendor_transaction_id": "2000000123456789", + "vendor_original_transaction_id": "2000000000000001", + "is_active": true, + "is_lifetime": false, + "activated_at": "2026-01-15T09:30:00.000Z", + "renewed_at": "2026-07-15T09:30:00.000Z", + "expires_at": "2026-08-15T09:30:00.000Z", + "starts_at": "2026-01-15T09:30:00.000Z", + "unsubscribed_at": "2026-07-20T10:00:00.000Z", + "billing_issue_detected_at": "2026-07-19T10:00:00.000Z", + "is_in_grace_period": false, + "is_refund": false, + "is_sandbox": true, + "will_renew": true, + "active_introductory_offer_type": "free_trial", + "active_promotional_offer_type": "promotional", + "active_promotional_offer_id": "promo-winter", + "offer_id": "offer-1", + "cancellation_reason": "voluntarily_cancelled" + } + }, + "non_subscriptions": { + "com.adapty.sample.coins": [ + { + "purchase_id": "a1b2c3d4-0000-4444-8888-999900001111", + "store": "app_store", + "vendor_product_id": "com.adapty.sample.coins", + "vendor_transaction_id": "2000000987654321", + "purchased_at": "2026-06-01T12:00:00.000Z", + "is_sandbox": true, + "is_refund": false, + "is_consumable": true + } + ] + }, + "timestamp": 1753876800000, + "is_test_user": true, + "applied_attribution_sources": ["appsflyer", "adjust"] +} diff --git a/tests/shared/Fixtures/responses/profile-minimal.json b/tests/shared/Fixtures/responses/profile-minimal.json new file mode 100644 index 0000000..2b163fe --- /dev/null +++ b/tests/shared/Fixtures/responses/profile-minimal.json @@ -0,0 +1,6 @@ +{ + "profile_id": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "segment_hash": "8f14e45fceea167a", + "timestamp": 1753876800000, + "is_test_user": false +} diff --git a/tests/shared/Fixtures/responses/purchase-result-cancelled.json b/tests/shared/Fixtures/responses/purchase-result-cancelled.json new file mode 100644 index 0000000..becf74c --- /dev/null +++ b/tests/shared/Fixtures/responses/purchase-result-cancelled.json @@ -0,0 +1,3 @@ +{ + "type": "user_cancelled" +} diff --git a/tests/shared/Fixtures/responses/purchase-result-pending.json b/tests/shared/Fixtures/responses/purchase-result-pending.json new file mode 100644 index 0000000..a5cdee4 --- /dev/null +++ b/tests/shared/Fixtures/responses/purchase-result-pending.json @@ -0,0 +1,3 @@ +{ + "type": "pending" +} diff --git a/tests/shared/Fixtures/responses/purchase-result-success.json b/tests/shared/Fixtures/responses/purchase-result-success.json new file mode 100644 index 0000000..45b692d --- /dev/null +++ b/tests/shared/Fixtures/responses/purchase-result-success.json @@ -0,0 +1,11 @@ +{ + "type": "success", + "apple_jws_transaction": "eyJhbGciOiJFUzI1NiJ9.payload.signature", + "google_purchase_token": "gpa.1234-5678-9012-34567", + "profile": { + "profile_id": "d3f4a1b2-0000-4c8d-9e2f-111122223333", + "segment_hash": "8f14e45fceea167a", + "timestamp": 1753876800000, + "is_test_user": false + } +} diff --git a/tests/shared/Fixtures/responses/user-action-full.json b/tests/shared/Fixtures/responses/user-action-full.json new file mode 100644 index 0000000..abe67a9 --- /dev/null +++ b/tests/shared/Fixtures/responses/user-action-full.json @@ -0,0 +1,7 @@ +{ + "action": { + "type": "open_url", + "value": "https://adapty.io/terms", + "open_in": "browser_out_app" + } +} diff --git a/tests/shared/Fixtures/responses/user-action-minimal.json b/tests/shared/Fixtures/responses/user-action-minimal.json new file mode 100644 index 0000000..023cfcf --- /dev/null +++ b/tests/shared/Fixtures/responses/user-action-minimal.json @@ -0,0 +1,5 @@ +{ + "action": { + "type": "close" + } +} diff --git a/tests/shared/ModelSnapshot.cs b/tests/shared/ModelSnapshot.cs new file mode 100644 index 0000000..33ccdf9 --- /dev/null +++ b/tests/shared/ModelSnapshot.cs @@ -0,0 +1,304 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace AdaptySDK.TestSupport +{ + /// + /// Renders the full state of a parsed model as deterministic JSON: every field, including + /// private ones, and every public property, keys sorted. Computed properties are invoked, and a + /// throwing one is recorded as throwing. + /// + public static class ModelSnapshot + { + private const int MaxDepth = 12; + + private static readonly System.Collections.Concurrent.ConcurrentDictionary< + Type, + List>> + > MemberCache = new System.Collections.Concurrent.ConcurrentDictionary< + Type, + List>> + >(); + + public static string Render(object value) + { + var builder = new StringBuilder(); + Write(builder, value, 0, new HashSet(ReferenceEqualityComparer.Instance)); + builder.AppendLine(); + return builder.ToString(); + } + + private static void Write(StringBuilder builder, object value, int depth, HashSet seen) + { + if (value is null) + { + builder.Append("null"); + return; + } + + var type = value.GetType(); + + if (type.IsEnum) + { + builder.Append(Quote(value.ToString() + " (" + Convert.ToInt64(value).ToString(CultureInfo.InvariantCulture) + ")")); + return; + } + + switch (value) + { + case string text: + builder.Append(Quote(text)); + return; + case bool flag: + builder.Append(flag ? "true" : "false"); + return; + case DateTime moment: + // Normalised to UTC so the snapshot does not depend on the machine's time + // zone, with Kind kept separately because the SDK's Local/Utc semantics matter. + builder.Append("{ \"utc\": ") + .Append(Quote(moment.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture))) + .Append(", \"kind\": ") + .Append(Quote(moment.Kind.ToString())) + .Append(" }"); + return; + case Guid guid: + builder.Append(Quote(guid.ToString())); + return; + case TimeSpan span: + builder.Append(Quote(span.ToString("c", CultureInfo.InvariantCulture))); + return; + case float single: + builder.Append(single.ToString("R", CultureInfo.InvariantCulture)); + return; + case double number: + builder.Append(number.ToString("R", CultureInfo.InvariantCulture)); + return; + case decimal dec: + builder.Append(dec.ToString(CultureInfo.InvariantCulture)); + return; + } + + if (type.IsPrimitive) + { + builder.Append(Convert.ToString(value, CultureInfo.InvariantCulture)); + return; + } + + if (depth >= MaxDepth) + { + builder.Append(Quote("")); + return; + } + + if (!seen.Add(value)) + { + builder.Append(Quote("")); + return; + } + + try + { + if (value is IDictionary dictionary) + { + WriteDictionary(builder, dictionary, depth, seen); + return; + } + + if (value is IEnumerable sequence) + { + WriteSequence(builder, sequence, depth, seen); + return; + } + + WriteObject(builder, value, type, depth, seen); + } + finally + { + seen.Remove(value); + } + } + + private static void WriteDictionary(StringBuilder builder, IDictionary dictionary, int depth, HashSet seen) + { + var keys = dictionary.Keys.Cast() + .OrderBy(key => Convert.ToString(key, CultureInfo.InvariantCulture), StringComparer.Ordinal) + .ToList(); + + if (keys.Count == 0) + { + builder.Append("{}"); + return; + } + + builder.Append('{'); + for (var i = 0; i < keys.Count; i++) + { + NewLine(builder, depth + 1); + builder.Append(Quote(Convert.ToString(keys[i], CultureInfo.InvariantCulture))).Append(": "); + Write(builder, dictionary[keys[i]], depth + 1, seen); + if (i < keys.Count - 1) + { + builder.Append(','); + } + } + NewLine(builder, depth); + builder.Append('}'); + } + + private static void WriteSequence(StringBuilder builder, IEnumerable sequence, int depth, HashSet seen) + { + var items = sequence.Cast().ToList(); + if (items.Count == 0) + { + builder.Append("[]"); + return; + } + + builder.Append('['); + for (var i = 0; i < items.Count; i++) + { + NewLine(builder, depth + 1); + Write(builder, items[i], depth + 1, seen); + if (i < items.Count - 1) + { + builder.Append(','); + } + } + NewLine(builder, depth); + builder.Append(']'); + } + + private static void WriteObject(StringBuilder builder, object value, Type type, int depth, HashSet seen) + { + var members = MemberCache.GetOrAdd(type, CollectMembers); + if (members.Count == 0) + { + builder.Append(Quote(value.ToString())); + return; + } + + builder.Append('{'); + NewLine(builder, depth + 1); + builder.Append("\"$type\": ").Append(Quote(type.Name)); + + foreach (var member in members) + { + builder.Append(','); + NewLine(builder, depth + 1); + builder.Append(Quote(member.Key)).Append(": "); + + object memberValue; + try + { + memberValue = member.Value(value); + } + catch (Exception e) + { + var inner = e is TargetInvocationException invocation && invocation.InnerException != null + ? invocation.InnerException + : e; + builder.Append(Quote("")); + continue; + } + + Write(builder, memberValue, depth + 1, seen); + } + + NewLine(builder, depth); + builder.Append('}'); + } + + private static List>> CollectMembers(Type type) + { + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + var members = new List>>(); + + foreach (var field in type.GetFields(flags)) + { + if (field.IsStatic || field.Name.Contains("<")) + { + continue; + } + + var captured = field; + members.Add(new KeyValuePair>( + captured.Name, + instance => captured.GetValue(instance) + )); + } + + foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (!property.CanRead || property.GetIndexParameters().Length > 0) + { + continue; + } + + var captured = property; + members.Add(new KeyValuePair>( + captured.Name + " (property)", + instance => captured.GetValue(instance) + )); + } + + return members.OrderBy(member => member.Key, StringComparer.Ordinal).ToList(); + } + + private static void NewLine(StringBuilder builder, int depth) + { + builder.Append('\n').Append(new string(' ', depth * 2)); + } + + private static string Quote(string text) + { + if (text is null) + { + return "null"; + } + + var builder = new StringBuilder(text.Length + 2); + builder.Append('"'); + foreach (var character in text) + { + switch (character) + { + case '"': + builder.Append("\\\""); + break; + case '\\': + builder.Append("\\\\"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + builder.Append(character); + break; + } + } + builder.Append('"'); + return builder.ToString(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + internal static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); + + public new bool Equals(object x, object y) => ReferenceEquals(x, y); + + public int GetHashCode(object obj) => + System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/tests/shared/Samples.cs b/tests/shared/Samples.cs new file mode 100644 index 0000000..955610a --- /dev/null +++ b/tests/shared/Samples.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace AdaptySDK.TestSupport +{ + /// + /// The objects the write tests serialize. Built through the package's own public API, so the + /// file stops compiling if that API changes shape. + /// + public static class Samples + { + public static AdaptyConfiguration Configuration() => + new AdaptyConfiguration.Builder("public_live_key") + .SetCustomerUserId("user-1", Guid.Empty, "obfuscated-1") + .SetObserverMode(true) + .SetIPAddressCollectionDisabled(true) + .SetServerCluster(AdaptyServerCluster.EU) + .SetBackendProxy("proxy.example.com", 8080) + .SetAdaptyUIMediaCache(100, 200, 300) + .Build(); + + /// + /// The default cluster is an explicit contract value, not the absence of one - a member + /// without its own contract name would serialize as the CLR name instead. + /// + public static AdaptyConfiguration ConfigurationWithDefaultCluster() => + new AdaptyConfiguration.Builder("public_live_key") + .SetServerCluster(AdaptyServerCluster.Default) + .Build(); + + /// + /// An identity with neither of its platform tokens set is dropped, not sent empty. + /// + public static AdaptyConfiguration ConfigurationWithEmptyIdentity() => + new AdaptyConfiguration.Builder("public_live_key") + .SetCustomerUserId("user-1", Guid.Empty, null) + .Build(); + + public static AdaptyProfileParameters ProfileParameters() + { + var parameters = new AdaptyProfileParameters.Builder() + .SetFirstName("Ada") + .SetLastName("Lovelace") + .SetGender(AdaptyProfileGender.Female) + .SetBirthday(new DateTime(1815, 12, 10)) + .SetEmail("ada@example.com") + .SetPhoneNumber("+15550100") + .SetAnalyticsDisabled(false) + .Build(); + parameters.SetCustomStringAttribute("plan", "gold"); + parameters.SetCustomDoubleAttribute("score", 12.5); + return parameters; + } + + public static AdaptyProductIdentifier ProductIdentifier() => + new AdaptyProductIdentifier( + "com.adapty.sample.monthly", + "adapty-product-1", + "monthly-base-plan" + ); + + /// + /// No base plan: an App Store product never has one, and the key is left out rather than + /// sent empty. + /// + public static AdaptyProductIdentifier ProductIdentifierWithoutBasePlan() => + new AdaptyProductIdentifier("com.adapty.sample.lifetime", "adapty-product-2", null); + + /// + /// The same identifier built with an empty base plan rather than none. It has to reach the + /// wire as the one above: null is what NullValueHandling drops, and an app that read the id + /// out of a text field has an empty string, not a null. + /// + public static AdaptyProductIdentifier ProductIdentifierWithEmptyBasePlan() => + new AdaptyProductIdentifier("com.adapty.sample.lifetime", "adapty-product-2", ""); + + public static AdaptyUIDialogConfiguration DialogConfiguration() => + new AdaptyUIDialogConfiguration() + .SetTitle("Cancel subscription?") + .SetContent("You keep access until the end of the period.") + .SetDefaultActionTitle("Keep") + .SetSecondaryActionTitle("Cancel"); + + /// + /// Only the required action title - the rest of the dialog is optional. + /// + public static AdaptyUIDialogConfiguration DialogConfigurationMinimal() => + new AdaptyUIDialogConfiguration().SetDefaultActionTitle("OK"); + + public static AdaptyPlacementFetchPolicy FetchPolicyDefault() => + AdaptyPlacementFetchPolicy.Default; + + public static AdaptyPlacementFetchPolicy FetchPolicyWithMaxAge() => + AdaptyPlacementFetchPolicy.ReturnCacheDataIfNotExpiredElseLoad( + TimeSpan.FromMinutes(1.5) + ); + + /// + /// One asset of every kind, so the platform-dependent paths and both colour forms are all + /// exercised in a single payload. + /// + public static Dictionary CustomAssets() + { + var gradient = new Gradient + { + colorKeys = new[] + { + new GradientColorKey(new Color(1f, 0f, 0f, 1f), 0f), + new GradientColorKey(new Color(0f, 0f, 1f, 1f), 1f), + }, + alphaKeys = new[] + { + new GradientAlphaKey(1f, 0f), + new GradientAlphaKey(0.5f, 0.25f), + new GradientAlphaKey(0f, 1f), + }, + }; + + return new Dictionary + { + { "hero_data", AdaptyCustomAsset.LocalImageData(new byte[] { 1, 2, 3, 250 }) }, + { "hero_asset", AdaptyCustomAsset.LocalImageAsset("hero-asset-id") }, + { "hero_file", AdaptyCustomAsset.LocalImageFile("images/hero.png") }, + { "clip_asset", AdaptyCustomAsset.LocalVideoAsset("clip-asset-id") }, + { "clip_file", AdaptyCustomAsset.LocalVideoFile("videos/clip.mp4") }, + { "accent", AdaptyCustomAsset.Color(new Color(0.2f, 0.4f, 0.6f, 1f)) }, + { "backdrop", AdaptyCustomAsset.LinearGradient(gradient) }, + }; + } + } +} diff --git a/tests/shared/Snapshots.cs b/tests/shared/Snapshots.cs new file mode 100644 index 0000000..1a67261 --- /dev/null +++ b/tests/shared/Snapshots.cs @@ -0,0 +1,179 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using NUnit.Framework; + +namespace AdaptySDK.TestSupport +{ + /// + /// Fixture loading and approval-style assertions. An approved snapshot is the layer's reference + /// behaviour, so a diff is either a regression or a change worth recording deliberately. + /// Set ADAPTY_UPDATE_SNAPSHOTS=1 to rewrite the approved files instead of failing. + /// + public static class Snapshots + { +#if UNITY_IOS + public const string Platform = "ios"; +#elif UNITY_ANDROID + public const string Platform = "android"; +#else + public const string Platform = "editor"; +#endif + + private static readonly string ProjectDirectory = ResolveProjectDirectory(); + + private static string FixturesDirectory => Path.Combine(ProjectDirectory, "Fixtures"); + + public static string LoadResponse(string name) => + File.ReadAllText(Path.Combine(FixturesDirectory, "responses", name + ".json")); + + /// + /// Sorts keys so snapshots compare what was sent, not the order it was written in, and folds + /// whole-numbered floats to integers - a difference no reader can observe. Fractional values + /// keep their digits, so real precision drift still shows. + /// + /// + /// Not JToken.Parse, for the reason AdaptyJson.ParseDocument avoids it too: its + /// reader defaults to DateParseHandling.DateTime, so every ISO string would be parsed + /// to a and printed back in Newtonsoft's own form. The snapshot would + /// then record that form rather than the one the SDK writes, and the format + /// AdaptyConverterDateTime emits - milliseconds and the Z designator included - + /// could change without moving a single approved file. + /// + public static string Canonical(string json) + { + using (var reader = new Newtonsoft.Json.JsonTextReader(new StringReader(json)) + { + DateParseHandling = Newtonsoft.Json.DateParseHandling.None, + FloatParseHandling = Newtonsoft.Json.FloatParseHandling.Double, + }) + { + var token = Newtonsoft.Json.Linq.JToken.Load(reader); + return Sort(token).ToString(Newtonsoft.Json.Formatting.Indented) + "\n"; + } + } + + private static Newtonsoft.Json.Linq.JToken Sort(Newtonsoft.Json.Linq.JToken token) + { + if (token is Newtonsoft.Json.Linq.JObject map) + { + var sorted = new Newtonsoft.Json.Linq.JObject(); + var keys = new System.Collections.Generic.List(); + foreach (var property in map.Properties()) + { + keys.Add(property.Name); + } + keys.Sort(System.StringComparer.Ordinal); + foreach (var key in keys) + { + sorted.Add(key, Sort(map[key])); + } + return sorted; + } + + if (token is Newtonsoft.Json.Linq.JArray array) + { + var sorted = new Newtonsoft.Json.Linq.JArray(); + foreach (var item in array) + { + sorted.Add(Sort(item)); + } + return sorted; + } + + if (token.Type == Newtonsoft.Json.Linq.JTokenType.Float) + { + var number = ((Newtonsoft.Json.Linq.JValue)token).ToObject(); + if (number == Math.Floor(number) && !double.IsInfinity(number)) + { + return new Newtonsoft.Json.Linq.JValue((long)number); + } + } + + return token; + } + + /// + /// Hides the SDK version, so a version bump does not fail unrelated snapshots. That the + /// field is there is worth pinning; today's number is not. + /// + private static string Normalize(string snapshot) => + snapshot.Replace(Adapty.SDKVersion, ""); + + public static void Matches(string name, string actual) + { + actual = Normalize(actual); + + var approvedDirectory = Path.Combine(FixturesDirectory, "approved"); + Directory.CreateDirectory(approvedDirectory); + + var approvedPath = Path.Combine(approvedDirectory, name + "." + Platform + ".approved.txt"); + var updating = Environment.GetEnvironmentVariable("ADAPTY_UPDATE_SNAPSHOTS") == "1"; + + if (updating || !File.Exists(approvedPath)) + { + File.WriteAllText(approvedPath, actual); + + if (!updating) + { + Assert.Fail( + $"No approved snapshot for '{name}'. One was written to {approvedPath}; " + + "review it and re-run." + ); + } + + return; + } + + var approved = File.ReadAllText(approvedPath); + if (approved == actual) + { + return; + } + + var receivedPath = Path.Combine(approvedDirectory, name + "." + Platform + ".received.txt"); + File.WriteAllText(receivedPath, actual); + + Assert.Fail( + $"Snapshot '{name}' differs from the approved one.\n" + + $" approved: {approvedPath}\n" + + $" received: {receivedPath}\n\n" + + Diff(approved, actual) + ); + } + + private static string Diff(string approved, string actual) + { + var approvedLines = approved.Replace("\r\n", "\n").Split('\n'); + var actualLines = actual.Replace("\r\n", "\n").Split('\n'); + var lines = Math.Max(approvedLines.Length, actualLines.Length); + var report = new System.Text.StringBuilder(); + var shown = 0; + + for (var i = 0; i < lines && shown < 20; i++) + { + var left = i < approvedLines.Length ? approvedLines[i] : ""; + var right = i < actualLines.Length ? actualLines[i] : ""; + if (left == right) + { + continue; + } + + report.AppendLine($" line {i + 1}:"); + report.AppendLine($" - {left}"); + report.AppendLine($" + {right}"); + shown++; + } + + if (shown == 0) + { + report.AppendLine(" (only trailing whitespace differs)"); + } + + return report.ToString(); + } + + private static string ResolveProjectDirectory([CallerFilePath] string callerPath = null) => + Path.GetDirectoryName(callerPath); + } +} diff --git a/tests/shared/UnityStubs.cs b/tests/shared/UnityStubs.cs new file mode 100644 index 0000000..e13c719 --- /dev/null +++ b/tests/shared/UnityStubs.cs @@ -0,0 +1,236 @@ +// Minimal UnityEngine stand-ins so the SDK sources link outside the Unity Editor. +// The models need [Preserve], and AdaptyCustomAsset.cs also Color and Gradient; the rest is for +// the platform bridges in Runtime/Plugins. + +using System; + +namespace UnityEngine +{ + public struct Color + { + public float r, g, b, a; + + public Color(float r, float g, float b, float a) + { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + } + + public struct GradientColorKey + { + public Color color; + public float time; + + public GradientColorKey(Color color, float time) + { + this.color = color; + this.time = time; + } + } + + public struct GradientAlphaKey + { + public float alpha; + public float time; + + public GradientAlphaKey(float alpha, float time) + { + this.alpha = alpha; + this.time = time; + } + } + + public class Gradient + { + public GradientColorKey[] colorKeys = new GradientColorKey[0]; + public GradientAlphaKey[] alphaKeys = new GradientAlphaKey[0]; + + // Linear interpolation over the colour keys, with alpha taken from the alpha keys. + // Enough to reproduce what the SDK serializes for a Unity gradient. + public Color Evaluate(float time) + { + var color = Sample(time); + return new Color(color.r, color.g, color.b, SampleAlpha(time)); + } + + private Color Sample(float time) + { + if (colorKeys.Length == 0) + { + return new Color(0, 0, 0, 1); + } + + var previous = colorKeys[0]; + foreach (var key in colorKeys) + { + if (key.time >= time) + { + if (key.time == previous.time) + { + return key.color; + } + + var t = (time - previous.time) / (key.time - previous.time); + return new Color( + previous.color.r + (key.color.r - previous.color.r) * t, + previous.color.g + (key.color.g - previous.color.g) * t, + previous.color.b + (key.color.b - previous.color.b) * t, + 1 + ); + } + + previous = key; + } + + return colorKeys[colorKeys.Length - 1].color; + } + + private float SampleAlpha(float time) + { + if (alphaKeys.Length == 0) + { + return 1f; + } + + var previous = alphaKeys[0]; + foreach (var key in alphaKeys) + { + if (key.time >= time) + { + if (key.time == previous.time) + { + return key.alpha; + } + + var t = (time - previous.time) / (key.time - previous.time); + return previous.alpha + (key.alpha - previous.alpha) * t; + } + + previous = key; + } + + return alphaKeys[alphaKeys.Length - 1].alpha; + } + } + + public static class Mathf + { + // Unity's Mathf.RoundToInt is (int)Math.Round(f), which rounds a midpoint to even. Rounding + // away from zero here instead would make the approved colour and gradient hexes disagree + // with what a real player sends for any channel landing exactly on .5. + public static int RoundToInt(float f) => (int)Math.Round((double)f); + } + + public static class Application + { + public static string dataPath => "/stub/dataPath"; + } + + public static class Debug + { + public static void Log(object message) => Console.WriteLine(message); + + public static void LogWarning(object message) => Console.WriteLine("WARN: " + message); + + public static void LogError(object message) => Console.WriteLine("ERROR: " + message); + } +} + +namespace UnityEngine +{ + // The Android bridge is linked by the next-package suite so the transport compiles under + // UNITY_ANDROID. Nothing calls into the JVM here - these only have to satisfy the compiler. + public class AndroidJavaObject : IDisposable + { + public AndroidJavaObject(string className, params object[] args) { } + + public T Call(string methodName, params object[] args) => + throw new NotSupportedException(); + + public void Call(string methodName, params object[] args) => + throw new NotSupportedException(); + + public T CallStatic(string methodName, params object[] args) => + throw new NotSupportedException(); + + public void CallStatic(string methodName, params object[] args) => + throw new NotSupportedException(); + + public T GetStatic(string fieldName) => throw new NotSupportedException(); + + public void Dispose() { } + } + + public class AndroidJavaClass : AndroidJavaObject + { + public AndroidJavaClass(string className) + : base(className) { } + } + + public class AndroidJavaProxy + { + protected AndroidJavaProxy(string javaInterface) { } + } +} + +namespace AOT +{ + // Marks the reverse-P/Invoke entry points on iOS. Attribute only - it changes nothing here, + // but the transport's callback boundary is exactly where a thrown exception would be fatal + // on IL2CPP, so the file has to compile in the test suite. + [AttributeUsage(AttributeTargets.Method)] + public sealed class MonoPInvokeCallbackAttribute : Attribute + { + public MonoPInvokeCallbackAttribute(Type type) { } + } +} + +namespace UnityEngine.Scripting +{ + // Unity's marker for "the linker cannot see this is used". Stubbed so the models keep + // compiling in the plain .NET suites. + [AttributeUsage( + AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Enum + | AttributeTargets.Constructor + | AttributeTargets.Method + | AttributeTargets.Property + | AttributeTargets.Field + | AttributeTargets.Event + | AttributeTargets.Interface + | AttributeTargets.Delegate + )] + public sealed class PreserveAttribute : Attribute { } +} + +namespace UnityEngine +{ + // Where Unity calls back into a static class before the first scene loads. Stubbed as an + // attribute only: the suites invoke the reset directly, which is the point of testing it. + public enum RuntimeInitializeLoadType + { + AfterSceneLoad = 0, + BeforeSceneLoad = 1, + AfterAssembliesLoaded = 2, + BeforeSplashScreen = 3, + SubsystemRegistration = 4, + } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RuntimeInitializeOnLoadMethodAttribute : Attribute + { + // Unity's own attribute keeps the load type; the stub does too, because which one a reset + // is registered for is the whole point of registering it. + public RuntimeInitializeOnLoadMethodAttribute() => + LoadType = RuntimeInitializeLoadType.AfterSceneLoad; + + public RuntimeInitializeOnLoadMethodAttribute(RuntimeInitializeLoadType loadType) => + LoadType = loadType; + + public RuntimeInitializeLoadType LoadType { get; } + } +} diff --git a/tests/surface/package/AdaptySDK.Surface.csproj b/tests/surface/package/AdaptySDK.Surface.csproj new file mode 100644 index 0000000..66f0ba9 --- /dev/null +++ b/tests/surface/package/AdaptySDK.Surface.csproj @@ -0,0 +1,63 @@ + + + + + + net8.0 + 9 + disable + disable + false + AdaptySDK.Surface + AdaptySDK + CS0169;CS0649;CS0414;CS0067 + false + + + true + + $(WarningsAsErrors);CS0419;CS1570;CS1571;CS1572;CS1573;CS1574;CS1580;CS1581;CS1584;CS1587;CS1589;CS1590;CS1592;CS1598;CS1710;CS1711;CS1712;CS1723;CS1734;CS1735 + $(MSBuildThisFileDirectory)../../../Packages/com.adapty.unity-sdk/Runtime + $(MSBuildThisFileDirectory)../../shared + + + + + UNITY_EDITOR + + + + $(DefineConstants);$(AdaptyPlatform) + + + + + + + + + + + + + + + + + + + + +