diff --git a/.github/workflows/manual_minor_prerelease.yml b/.github/workflows/manual_minor_prerelease.yml
deleted file mode 100644
index 31156f4..0000000
--- a/.github/workflows/manual_minor_prerelease.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: Manual Minor Prerelease
-
-on:
- workflow_dispatch
-
-jobs:
- patch-minor:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
- with:
- ref: develop
-
- - name: Minor
- run: |
- fastlane minor
diff --git a/.github/workflows/manual_patch_prerelease.yml b/.github/workflows/manual_patch_prerelease.yml
deleted file mode 100644
index 21a1403..0000000
--- a/.github/workflows/manual_patch_prerelease.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: Manual Patch Prerelease
-
-on:
- workflow_dispatch
-
-jobs:
- patch-prerelease:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
- with:
- ref: develop
-
- - name: Patch
- run: |
- fastlane patch
diff --git a/.github/workflows/prerelease_github.yml b/.github/workflows/prerelease_github.yml
deleted file mode 100644
index 2747386..0000000
--- a/.github/workflows/prerelease_github.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-name: Pre-release Github
-
-on:
- push:
- branches:
- - "main"
-
-jobs:
- pre-release:
- runs-on: macos-latest
-
- steps:
- - uses: "marvinpinto/action-automatic-releases@latest"
- with:
- repo_token: "${{ secrets.GITHUB_TOKEN }}"
- automatic_release_tag: "latest"
- prerelease: true
- title: "Development Build"
- files: |
- LICENSE.txt
- *.jar
\ No newline at end of file
diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml
new file mode 100644
index 0000000..0b4a6fb
--- /dev/null
+++ b/.github/workflows/publish_release.yml
@@ -0,0 +1,14 @@
+name: Publish release
+
+on:
+ pull_request:
+ types: [closed]
+ branches:
+ - main
+
+jobs:
+ publish_release:
+ if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') && contains(github.event.pull_request.labels.*.name, 'one-click-release')
+ uses: qonversion/shared-sdk-workflows/.github/workflows/publish_release.yml@main
+ secrets:
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..502404a
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,32 @@
+name: Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ bump:
+ description: 'Version part to bump'
+ required: true
+ type: choice
+ options:
+ - patch
+ - minor
+ - major
+ default: patch
+ version:
+ description: 'Explicit release version like 5.13.2 (overrides bump)'
+ required: false
+ type: string
+ release_notes:
+ description: 'Release notes markdown (empty — auto-generated from pull requests)'
+ required: false
+ type: string
+
+jobs:
+ release:
+ uses: qonversion/shared-sdk-workflows/.github/workflows/release.yml@main
+ with:
+ bump: ${{ inputs.bump }}
+ version: ${{ inputs.version }}
+ release_notes: ${{ inputs.release_notes }}
+ secrets:
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
diff --git a/.github/workflows/release_pull_requests.yml b/.github/workflows/release_pull_requests.yml
deleted file mode 100644
index e4e0569..0000000
--- a/.github/workflows/release_pull_requests.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-name: Release pull requests from dev by tag
-on:
- push:
- tags:
- - prerelease/*
-
-jobs:
- handle_prerelease:
- uses: qonversion/shared-sdk-workflows/.github/workflows/prerelease_handling.yml@main
diff --git a/Editor/QonversionDependencies.xml b/Editor/QonversionDependencies.xml
index 076a981..4e7dc9e 100644
--- a/Editor/QonversionDependencies.xml
+++ b/Editor/QonversionDependencies.xml
@@ -1,11 +1,11 @@
-
+
-
+
diff --git a/Runtime/Android/Plugins/com/qonversion/unitywrapper/NoCodesWrapper.java b/Runtime/Android/Plugins/com/qonversion/unitywrapper/NoCodesWrapper.java
index ade86b6..437ec6f 100644
--- a/Runtime/Android/Plugins/com/qonversion/unitywrapper/NoCodesWrapper.java
+++ b/Runtime/Android/Plugins/com/qonversion/unitywrapper/NoCodesWrapper.java
@@ -15,9 +15,13 @@
import java.util.HashMap;
import java.util.Map;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
import io.qonversion.sandwich.NoCodesEventListener;
import io.qonversion.sandwich.NoCodesPurchaseDelegateBridge;
import io.qonversion.sandwich.NoCodesSandwich;
+import io.qonversion.sandwich.ResultListener;
+import io.qonversion.sandwich.SandwichError;
public class NoCodesWrapper {
private static final String TAG = "NoCodesWrapper";
@@ -28,6 +32,9 @@ public class NoCodesWrapper {
private static final String EVENT_ACTION_FINISHED = "OnNoCodesActionFinished";
private static final String EVENT_FINISHED = "OnNoCodesFinished";
private static final String EVENT_SCREEN_FAILED_TO_LOAD = "OnNoCodesScreenFailedToLoad";
+ private static final String EVENT_CUSTOM_ACTION = "OnNoCodesCustomAction";
+ private static final String EVENT_SCREEN_LOADED = "OnNoCodesScreenLoaded";
+ private static final String EVENT_SCREEN_LOAD_FAILED = "OnNoCodesScreenLoadFailed";
private static final String EVENT_PURCHASE = "OnNoCodesPurchase";
private static final String EVENT_RESTORE = "OnNoCodesRestore";
@@ -112,6 +119,27 @@ public static synchronized void showScreen(String contextKey, String customVaria
noCodesSandwich.showScreen(contextKey, customVariables);
}
+ public static synchronized void loadScreen(final String contextKey) {
+ if (noCodesSandwich == null) {
+ Log.e(TAG, "NoCodesSandwich is not initialized");
+ return;
+ }
+
+ noCodesSandwich.loadScreen(contextKey, new ResultListener() {
+ @Override
+ public void onSuccess(@NonNull Map data) {
+ sendMessageToUnity(data, EVENT_SCREEN_LOADED);
+ }
+
+ @Override
+ public void onError(@NonNull SandwichError error) {
+ ObjectNode payload = Utils.createErrorNode(error);
+ payload.put("contextKey", contextKey);
+ sendMessageToUnity(payload, EVENT_SCREEN_LOAD_FAILED);
+ }
+ });
+ }
+
public static synchronized void close() {
if (noCodesSandwich == null) {
Log.e(TAG, "NoCodesSandwich is not initialized");
@@ -208,6 +236,9 @@ public void onNoCodesEvent(@NonNull Event event, @Nullable Map data)
case ScreenFailedToLoad:
methodName = EVENT_SCREEN_FAILED_TO_LOAD;
break;
+ case CustomAction:
+ methodName = EVENT_CUSTOM_ACTION;
+ break;
default:
return;
}
diff --git a/Runtime/Scripts/Internal/QonversionInternal.cs b/Runtime/Scripts/Internal/QonversionInternal.cs
index 55df781..d7175ea 100644
--- a/Runtime/Scripts/Internal/QonversionInternal.cs
+++ b/Runtime/Scripts/Internal/QonversionInternal.cs
@@ -28,7 +28,7 @@ internal class QonversionInternal : MonoBehaviour, IQonversion
private const string OnIsFallbackFileAccessibleMethodName = "OnIsFallbackFileAccessible";
private const string OnPromotionalOfferMethodName = "OnPromotionalOffer";
- private const string SdkVersion = "9.6.0";
+ private const string SdkVersion = "9.7.0";
private const string SdkSource = "unity";
private const string DefaultRemoteConfigContextKey = "";
diff --git a/Runtime/Scripts/NoCodes/Android/NoCodesWrapperAndroid.cs b/Runtime/Scripts/NoCodes/Android/NoCodesWrapperAndroid.cs
index 2bfd47f..3fb822e 100644
--- a/Runtime/Scripts/NoCodes/Android/NoCodesWrapperAndroid.cs
+++ b/Runtime/Scripts/NoCodes/Android/NoCodesWrapperAndroid.cs
@@ -26,6 +26,11 @@ public void ShowScreen(string contextKey, string customVariablesJson)
CallNoCodes("showScreen", contextKey, customVariablesJson ?? "");
}
+ public void LoadScreen(string contextKey)
+ {
+ CallNoCodes("loadScreen", contextKey);
+ }
+
public void Close()
{
CallNoCodes("close");
diff --git a/Runtime/Scripts/NoCodes/Dto/NoCodesError.cs b/Runtime/Scripts/NoCodes/Dto/NoCodesError.cs
index 6fcc9aa..39d3de9 100644
--- a/Runtime/Scripts/NoCodes/Dto/NoCodesError.cs
+++ b/Runtime/Scripts/NoCodes/Dto/NoCodesError.cs
@@ -85,6 +85,7 @@ private static NoCodesErrorCode ParseErrorCode(string code)
case "RateLimitExceeded": return NoCodesErrorCode.RateLimitExceeded;
case "ScreenLoadingFailed": return NoCodesErrorCode.ScreenLoadingFailed;
case "SDKInitializationError": return NoCodesErrorCode.SdkInitializationError;
+ case "ClientError": return NoCodesErrorCode.ClientError;
default: return NoCodesErrorCode.Unknown;
}
}
diff --git a/Runtime/Scripts/NoCodes/Dto/NoCodesErrorCode.cs b/Runtime/Scripts/NoCodes/Dto/NoCodesErrorCode.cs
index b080cd3..8073f43 100644
--- a/Runtime/Scripts/NoCodes/Dto/NoCodesErrorCode.cs
+++ b/Runtime/Scripts/NoCodes/Dto/NoCodesErrorCode.cs
@@ -30,6 +30,7 @@ public enum NoCodesErrorCode
ProductsLoadingFailed,
RateLimitExceeded,
ScreenLoadingFailed,
- SdkInitializationError
+ SdkInitializationError,
+ ClientError
}
}
diff --git a/Runtime/Scripts/NoCodes/Dto/NoCodesScreen.cs b/Runtime/Scripts/NoCodes/Dto/NoCodesScreen.cs
new file mode 100644
index 0000000..f14a255
--- /dev/null
+++ b/Runtime/Scripts/NoCodes/Dto/NoCodesScreen.cs
@@ -0,0 +1,175 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using QonversionUnity.MiniJSON;
+
+namespace QonversionUnity
+{
+ ///
+ /// Kind of a No-Codes screen default variable — what it was configured as in the builder.
+ /// The set may grow in future backend versions; values this SDK version does not know
+ /// are mapped to instead of failing.
+ ///
+ public enum NoCodesScreenVariableKind
+ {
+ /// A Screen Variable authored in the builder's Variables section.
+ Custom,
+
+ ///
+ /// A product slot: the variable key is the slot name and the value is the default
+ /// Qonversion product id assigned to it.
+ ///
+ Product,
+
+ ///
+ /// The screen's Default Product configured in the builder: the value is
+ /// the Qonversion product id selected by default.
+ ///
+ SelectedProduct,
+
+ /// A kind introduced on the backend after this SDK version was released.
+ Unknown
+ }
+
+ ///
+ /// A typed default variable of a No-Codes screen, configured in the builder and delivered
+ /// at screen load so it can be read by key. The value keeps its authored type
+ /// (bool / string / number) rather than being coerced to a string.
+ ///
+ public class NoCodesScreenVariable
+ {
+ /// What the variable represents — see .
+ public readonly NoCodesScreenVariableKind Kind;
+
+ ///
+ /// Variable name it is addressed by (`variable.<key>` in the builder for custom
+ /// variables, the slot name for product slots). May contain spaces.
+ ///
+ public readonly string Key;
+
+ /// Authored value type: "boolean", "string" or "number".
+ public readonly string Type;
+
+ ///
+ /// The configured default value, preserving its native type (bool, string, long or double).
+ /// Null when no default value was authored.
+ ///
+ [CanBeNull] public readonly object Value;
+
+ ///
+ /// The value rendered as a plain string regardless of its native type: "true"/"false"
+ /// for booleans, the string itself, a number without a trailing ".0" when integral,
+ /// or an empty string when no value was authored.
+ ///
+ public readonly string StringValue;
+
+ public NoCodesScreenVariable(Dictionary dict)
+ {
+ Kind = ParseKind(dict.GetString("kind"));
+ Key = dict.GetString("key");
+ Type = dict.GetString("type");
+ dict.TryGetValue("value", out Value);
+ StringValue = dict.GetString("stringValue");
+ }
+
+ private static NoCodesScreenVariableKind ParseKind([CanBeNull] string kind)
+ {
+ switch (kind)
+ {
+ case "custom": return NoCodesScreenVariableKind.Custom;
+ case "product": return NoCodesScreenVariableKind.Product;
+ case "selected_product": return NoCodesScreenVariableKind.SelectedProduct;
+ default: return NoCodesScreenVariableKind.Unknown;
+ }
+ }
+
+ public override string ToString()
+ {
+ return $"{nameof(Kind)}: {Kind}, " +
+ $"{nameof(Key)}: {Key}, " +
+ $"{nameof(Type)}: {Type}, " +
+ $"{nameof(Value)}: {Value}, " +
+ $"{nameof(StringValue)}: {StringValue}";
+ }
+ }
+
+ ///
+ /// A loaded No-Codes screen returned from .
+ ///
+ /// Exposes the screen identifiers and the typed default variables configured in the builder —
+ /// the screen content stays internal, as rendering remains the SDK's job via
+ /// .
+ ///
+ public class NoCodesScreen
+ {
+ /// Identifier of the screen.
+ public readonly string Id;
+
+ /// The context key of the screen set in the No-Codes builder.
+ public readonly string ContextKey;
+
+ ///
+ /// The Qonversion product id selected by default when the screen opens (the builder's
+ /// Default Product), or null when none is configured.
+ ///
+ [CanBeNull] public readonly string DefaultSelectedProductId;
+
+ ///
+ /// Typed default variables of the screen configured in the builder: authored custom
+ /// variables and product slots. Read them by (may be empty).
+ ///
+ public readonly List DefaultVariables = new List();
+
+ public NoCodesScreen(Dictionary dict)
+ {
+ Id = dict.GetString("id");
+ ContextKey = dict.GetString("contextKey");
+ DefaultSelectedProductId = dict.TryGetValue("defaultSelectedProductId", out object productId)
+ ? productId as string
+ : null;
+
+ if (dict.TryGetValue("defaultVariables", out object variablesValue) && variablesValue is List