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 rawVariables) + { + foreach (object rawVariable in rawVariables) + { + if (rawVariable is Dictionary variableDict) + { + DefaultVariables.Add(new NoCodesScreenVariable(variableDict)); + } + } + } + } + + /// + /// Returns the default variable configured under the given key, or null when the screen + /// has no variable with that exact (case-sensitive) key. + /// + /// Keys are only unique within a kind — a custom variable and a product slot may share + /// a name — so pass to disambiguate; without it the first match + /// in payload order (custom variables, then product slots, then the selected product) + /// is returned. + /// + /// For the default selected product prefer — it needs no key. + /// + [CanBeNull] + public NoCodesScreenVariable DefaultVariable(string key, NoCodesScreenVariableKind? kind = null) + { + foreach (NoCodesScreenVariable variable in DefaultVariables) + { + if (variable.Key == key && (kind == null || variable.Kind == kind)) + { + return variable; + } + } + + return null; + } + + public override string ToString() + { + return $"{nameof(Id)}: {Id}, " + + $"{nameof(ContextKey)}: {ContextKey}, " + + $"{nameof(DefaultSelectedProductId)}: {DefaultSelectedProductId}, " + + $"{nameof(DefaultVariables)}: {string.Join("; ", DefaultVariables)}"; + } + } +} diff --git a/Runtime/Scripts/NoCodes/Dto/NoCodesScreen.cs.meta b/Runtime/Scripts/NoCodes/Dto/NoCodesScreen.cs.meta new file mode 100644 index 0000000..7da7b8b --- /dev/null +++ b/Runtime/Scripts/NoCodes/Dto/NoCodesScreen.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9ac2daf1b3c146a4a8f458088984dd5c \ No newline at end of file diff --git a/Runtime/Scripts/NoCodes/INoCodes.cs b/Runtime/Scripts/NoCodes/INoCodes.cs index 4160ca4..db61fa5 100644 --- a/Runtime/Scripts/NoCodes/INoCodes.cs +++ b/Runtime/Scripts/NoCodes/INoCodes.cs @@ -24,6 +24,19 @@ public interface INoCodes /// and only applied to that screen. void ShowScreen(string contextKey, [CanBeNull] System.Collections.Generic.Dictionary customVariables = null); + /// + /// Load a No-Code screen (from cache or network) without presenting it, so you can decide + /// whether to present it or show your own fallback UI before any SDK screen appears. + /// Present the screen with — the loaded content is served from cache. + /// + /// The returned exposes the typed default variables configured + /// in the builder and the default selected product id. + /// + /// The context key of the screen to load. + /// Called with the loaded screen data. + /// Called when loading fails. + void LoadScreen(string contextKey, System.Action onSuccess, System.Action onError); + /// /// Close the current opened No-Code screen. /// diff --git a/Runtime/Scripts/NoCodes/Internal/INoCodesWrapper.cs b/Runtime/Scripts/NoCodes/Internal/INoCodesWrapper.cs index 26efd27..2724630 100644 --- a/Runtime/Scripts/NoCodes/Internal/INoCodesWrapper.cs +++ b/Runtime/Scripts/NoCodes/Internal/INoCodesWrapper.cs @@ -8,6 +8,7 @@ internal interface INoCodesWrapper void SetDelegate(); void SetScreenPresentationConfig(string configJson, [CanBeNull] string contextKey); void ShowScreen(string contextKey, [CanBeNull] string customVariablesJson); + void LoadScreen(string contextKey); void Close(); void SetLocale([CanBeNull] string locale); void SetTheme(string theme); diff --git a/Runtime/Scripts/NoCodes/Internal/NoCodesInternal.cs b/Runtime/Scripts/NoCodes/Internal/NoCodesInternal.cs index 1c6b80b..6340004 100644 --- a/Runtime/Scripts/NoCodes/Internal/NoCodesInternal.cs +++ b/Runtime/Scripts/NoCodes/Internal/NoCodesInternal.cs @@ -12,6 +12,17 @@ internal class NoCodesInternal : MonoBehaviour, INoCodes private NoCodesPurchaseDelegate _purchaseDelegate; private NoCodesConfig _config; + private struct LoadScreenCallbacks + { + public System.Action OnSuccess; + public System.Action OnError; + } + + // Load requests are correlated by contextKey: both the success payload (the screen map) + // and the native failure payload carry it. + private readonly System.Collections.Generic.Dictionary> _loadScreenCallbacks = + new System.Collections.Generic.Dictionary>(); + public static NoCodesInternal CreateInstance(NoCodesConfig config) { GameObject go = new GameObject(GameObjectName); @@ -58,6 +69,28 @@ public void ShowScreen(string contextKey, System.Collections.Generic.Dictionary< wrapper.ShowScreen(contextKey, customVariablesJson); } + public void LoadScreen(string contextKey, System.Action onSuccess, System.Action onError) + { + INoCodesWrapper wrapper = GetNativeWrapper(); + if (wrapper is NoCodesWrapperNoop) + { + // The noop wrapper never sends anything back — fail synchronously + // instead of leaving the request pending forever. + onError?.Invoke(NoCodesMapper.UnsupportedPlatformError()); + return; + } + + if (!_loadScreenCallbacks.TryGetValue(contextKey, out var callbacks)) + { + callbacks = new System.Collections.Generic.List(); + _loadScreenCallbacks[contextKey] = callbacks; + } + + callbacks.Add(new LoadScreenCallbacks { OnSuccess = onSuccess, OnError = onError }); + + wrapper.LoadScreen(contextKey); + } + public void Close() { INoCodesWrapper wrapper = GetNativeWrapper(); @@ -199,6 +232,60 @@ private void OnNoCodesScreenFailedToLoad(string jsonString) } } + private void OnNoCodesCustomAction(string jsonString) + { + if (!(_noCodesDelegate is NoCodesCustomActionDelegate customActionDelegate)) return; + + string value = NoCodesMapper.CustomActionValueFromJson(jsonString); + customActionDelegate.OnCustomAction(value); + } + + // Called from native with the loaded screen data + private void OnNoCodesScreenLoaded(string jsonString) + { + NoCodesScreen screen = NoCodesMapper.ScreenFromJson(jsonString); + if (screen == null) + { + // Don't leave the request pending on an unparseable payload — fail the + // callbacks the payload's contextKey still lets us correlate. + string contextKey = NoCodesMapper.ContextKeyFromJson(jsonString); + NoCodesError parseError = NoCodesMapper.ScreenParsingError(); + foreach (LoadScreenCallbacks callbacks in TakeLoadScreenCallbacks(contextKey)) + { + callbacks.OnError?.Invoke(parseError); + } + return; + } + + foreach (LoadScreenCallbacks callbacks in TakeLoadScreenCallbacks(screen.ContextKey)) + { + callbacks.OnSuccess?.Invoke(screen); + } + } + + // Called from native when loading a screen failed; payload: { contextKey, error } + private void OnNoCodesScreenLoadFailed(string jsonString) + { + string contextKey = NoCodesMapper.ContextKeyFromJson(jsonString); + NoCodesError error = NoCodesMapper.LoadScreenErrorFromJson(jsonString); + + foreach (LoadScreenCallbacks callbacks in TakeLoadScreenCallbacks(contextKey)) + { + callbacks.OnError?.Invoke(error); + } + } + + private System.Collections.Generic.List TakeLoadScreenCallbacks(string contextKey) + { + if (contextKey == null || !_loadScreenCallbacks.TryGetValue(contextKey, out var callbacks)) + { + return new System.Collections.Generic.List(); + } + + _loadScreenCallbacks.Remove(contextKey); + return callbacks; + } + // Called from native for purchase delegate private void OnNoCodesPurchase(string jsonString) { diff --git a/Runtime/Scripts/NoCodes/Internal/NoCodesMapper.cs b/Runtime/Scripts/NoCodes/Internal/NoCodesMapper.cs index 1917c1e..4c1a557 100644 --- a/Runtime/Scripts/NoCodes/Internal/NoCodesMapper.cs +++ b/Runtime/Scripts/NoCodes/Internal/NoCodesMapper.cs @@ -43,6 +43,88 @@ internal static NoCodesError ErrorFromJson(string jsonStr) return new NoCodesError(dict); } + [CanBeNull] + internal static NoCodesScreen ScreenFromJson(string jsonStr) + { + if (!(Json.Deserialize(jsonStr) is Dictionary screenDict)) + { + Debug.LogError("Could not parse NoCodes screen"); + return null; + } + + try + { + return new NoCodesScreen(screenDict); + } + catch (System.Exception e) + { + // An anomalous payload (e.g. null field values) must fail the pending + // LoadScreen callbacks, not escape into the UnitySendMessage handler. + Debug.LogError("Could not map NoCodes screen: " + e.Message); + return null; + } + } + + internal static string CustomActionValueFromJson(string jsonStr) + { + if (!(Json.Deserialize(jsonStr) is Dictionary dict)) + { + Debug.LogError("Could not parse NoCodes custom action"); + return ""; + } + + return dict.GetString("value"); + } + + [CanBeNull] + internal static string ContextKeyFromJson(string jsonStr) + { + if (!(Json.Deserialize(jsonStr) is Dictionary dict)) + { + return null; + } + + return dict.GetString("contextKey"); + } + + internal static NoCodesError ScreenParsingError() + { + return new NoCodesError(new Dictionary + { + { "code", "Deserialization" }, + { "description", "Failed to parse the loaded No-Code screen" }, + { "additionalMessage", "Native payload parsing failed." } + }); + } + + internal static NoCodesError UnsupportedPlatformError() + { + return new NoCodesError(new Dictionary + { + { "code", "Unknown" }, + { "description", "No-Codes is not supported on this platform" }, + { "additionalMessage", "" } + }); + } + + internal static NoCodesError LoadScreenErrorFromJson(string jsonStr) + { + if (Json.Deserialize(jsonStr) is Dictionary dict && + dict.TryGetValue("error", out object errorValue) && + errorValue is Dictionary errorDict) + { + return new NoCodesError(errorDict); + } + + Debug.LogError("Could not parse NoCodes screen loading error"); + return new NoCodesError(new Dictionary + { + { "code", "Unknown" }, + { "description", "Failed to load No-Code screen" }, + { "additionalMessage", "Native error parsing failed." } + }); + } + [CanBeNull] internal static Product ProductFromJson(string jsonStr) { diff --git a/Runtime/Scripts/NoCodes/Internal/NoCodesWrapperNoop.cs b/Runtime/Scripts/NoCodes/Internal/NoCodesWrapperNoop.cs index 1f10b2b..da9842f 100644 --- a/Runtime/Scripts/NoCodes/Internal/NoCodesWrapperNoop.cs +++ b/Runtime/Scripts/NoCodes/Internal/NoCodesWrapperNoop.cs @@ -27,6 +27,11 @@ public void ShowScreen(string contextKey, string customVariablesJson) Debug.Log($"NoCodes.ShowScreen({contextKey}) called on unsupported platform."); } + public void LoadScreen(string contextKey) + { + Debug.Log($"NoCodes.LoadScreen({contextKey}) called on unsupported platform."); + } + public void Close() { // No-op diff --git a/Runtime/Scripts/NoCodes/NoCodesCustomActionDelegate.cs b/Runtime/Scripts/NoCodes/NoCodesCustomActionDelegate.cs new file mode 100644 index 0000000..ac44ebb --- /dev/null +++ b/Runtime/Scripts/NoCodes/NoCodesCustomActionDelegate.cs @@ -0,0 +1,22 @@ +namespace QonversionUnity +{ + /// + /// Optional companion to for receiving custom actions + /// configured in the No-Codes builder. + /// + /// Implement this interface on the same class as your — + /// it is a separate interface (instead of a new method) + /// to keep existing delegate implementations source-compatible. + /// + public interface NoCodesCustomActionDelegate + { + /// + /// Called when a custom action configured in the builder is triggered on the screen. + /// The No-Codes SDK does not execute anything itself — handle the value in your app code. + /// The screen stays open; close it using if needed. + /// + /// The string value configured for the custom action in the builder, + /// or an empty string if no value was configured. + void OnCustomAction(string value); + } +} diff --git a/Runtime/Scripts/NoCodes/NoCodesCustomActionDelegate.cs.meta b/Runtime/Scripts/NoCodes/NoCodesCustomActionDelegate.cs.meta new file mode 100644 index 0000000..10f2696 --- /dev/null +++ b/Runtime/Scripts/NoCodes/NoCodesCustomActionDelegate.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 67063ead83f74bdfbf1d68ebdf1cc07f \ No newline at end of file diff --git a/Runtime/Scripts/NoCodes/iOS/NoCodesWrapperIOS.cs b/Runtime/Scripts/NoCodes/iOS/NoCodesWrapperIOS.cs index 53d5a51..2018bfd 100644 --- a/Runtime/Scripts/NoCodes/iOS/NoCodesWrapperIOS.cs +++ b/Runtime/Scripts/NoCodes/iOS/NoCodesWrapperIOS.cs @@ -19,6 +19,9 @@ internal class NoCodesWrapperIOS : INoCodesWrapper [DllImport("__Internal")] private static extern void _showNoCodesScreen(string contextKey, string customVariablesJson); + [DllImport("__Internal")] + private static extern void _loadNoCodesScreen(string contextKey); + [DllImport("__Internal")] private static extern void _closeNoCodes(); @@ -72,6 +75,13 @@ public void ShowScreen(string contextKey, string customVariablesJson) #endif } + public void LoadScreen(string contextKey) + { +#if UNITY_IOS + _loadNoCodesScreen(contextKey); +#endif + } + public void Close() { #if UNITY_IOS diff --git a/Runtime/iOS/Plugins/NoCodes/NoCodesBridge.m b/Runtime/iOS/Plugins/NoCodes/NoCodesBridge.m index 9dabbc8..141b06c 100644 --- a/Runtime/iOS/Plugins/NoCodes/NoCodesBridge.m +++ b/Runtime/iOS/Plugins/NoCodes/NoCodesBridge.m @@ -50,6 +50,11 @@ void _showNoCodesScreen(const char* contextKey, const char* customVariablesJson) [noCodesBridge showScreen:contextKeyStr customVariables:customVariables]; } +void _loadNoCodesScreen(const char* contextKey) { + NSString *contextKeyStr = [UtilityBridge convertCStringToNSString:contextKey]; + [noCodesBridge loadScreen:contextKeyStr]; +} + void _closeNoCodes() { [noCodesBridge close]; } diff --git a/Runtime/iOS/Plugins/NoCodes/QNUNoCodesDelegate.h b/Runtime/iOS/Plugins/NoCodes/QNUNoCodesDelegate.h index 0e6a179..ca23690 100644 --- a/Runtime/iOS/Plugins/NoCodes/QNUNoCodesDelegate.h +++ b/Runtime/iOS/Plugins/NoCodes/QNUNoCodesDelegate.h @@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setDelegate; - (void)setScreenPresentationConfig:(NSDictionary *)config contextKey:(NSString * _Nullable)contextKey; - (void)showScreen:(NSString *)contextKey customVariables:(NSDictionary * _Nullable)customVariables; +- (void)loadScreen:(NSString *)contextKey; - (void)close; - (void)setLocale:(NSString * _Nullable)locale; - (void)setTheme:(NSString * _Nullable)theme; diff --git a/Runtime/iOS/Plugins/NoCodes/QNUNoCodesDelegate.m b/Runtime/iOS/Plugins/NoCodes/QNUNoCodesDelegate.m index c6101ef..f3ba97a 100644 --- a/Runtime/iOS/Plugins/NoCodes/QNUNoCodesDelegate.m +++ b/Runtime/iOS/Plugins/NoCodes/QNUNoCodesDelegate.m @@ -12,6 +12,9 @@ static NSString *const kEventActionFinished = @"OnNoCodesActionFinished"; static NSString *const kEventFinished = @"OnNoCodesFinished"; static NSString *const kEventScreenFailedToLoad = @"OnNoCodesScreenFailedToLoad"; +static NSString *const kEventCustomAction = @"OnNoCodesCustomAction"; +static NSString *const kEventScreenLoaded = @"OnNoCodesScreenLoaded"; +static NSString *const kEventScreenLoadFailed = @"OnNoCodesScreenLoadFailed"; static NSString *const kEventPurchase = @"OnNoCodesPurchase"; static NSString *const kEventRestore = @"OnNoCodesRestore"; @@ -37,7 +40,8 @@ - (instancetype)initWithListenerName:(char *)unityListenerName { @"nocodes_action_failed": kEventActionFailed, @"nocodes_action_finished": kEventActionFinished, @"nocodes_finished": kEventFinished, - @"nocodes_screen_failed_to_load": kEventScreenFailedToLoad + @"nocodes_screen_failed_to_load": kEventScreenFailedToLoad, + @"nocodes_custom_action": kEventCustomAction }; } @@ -68,6 +72,18 @@ - (void)showScreen:(NSString *)contextKey customVariables:(NSDictionary * _Nullable result, SandwichError * _Nullable error) { + if (error) { + NSMutableDictionary *payload = [[UtilityBridge serializeSandwichError:error] mutableCopy]; + payload[@"contextKey"] = contextKey; + [UtilityBridge sendUnityMessage:payload toMethod:kEventScreenLoadFailed unityListener:noCodesListenerName]; + } else { + [UtilityBridge sendUnityMessage:result ?: @{} toMethod:kEventScreenLoaded unityListener:noCodesListenerName]; + } + }]; +} + - (void)close { [self.noCodesSandwich close]; } diff --git a/package.json b/package.json index 1cb9f0d..5d1bfc8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.qonversion.unity", "displayName": "Qonversion", - "version": "9.6.0", + "version": "9.7.0", "unity": "2018.3", "description": "Empower your mobile app marketing and product decisions with precise subscription data.", "author": {