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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion QonversionCapacitorPlugin.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ Pod::Spec.new do |s|
s.ios.deployment_target = '14.0'
s.dependency 'Capacitor'
s.swift_version = '5.1'
s.dependency "QonversionSandwich", "7.9.0"
s.dependency "QonversionSandwich", "7.10.1"
end
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ dependencies {
implementation project(':capacitor-android')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation 'androidx.core:core-ktx:1.13.1'
implementation "io.qonversion:sandwich:7.9.0"
implementation "io.qonversion:sandwich:7.10.1"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ class NoCodesPlugin : Plugin() {
call.resolve()
}

@PluginMethod
fun loadScreen(call: PluginCall) {
val contextKey = call.getString("contextKey")
?: return call.noNecessaryDataError("contextKey")

noCodesSandwich.loadScreen(contextKey, call.toResultListener())
}

@PluginMethod
fun close(call: PluginCall) {
noCodesSandwich.close()
Expand Down
25 changes: 25 additions & 0 deletions ios/Sources/QonversionPlugin/NoCodesPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class NoCodesPlugin: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setScreenPresentationConfig", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "showScreen", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "loadScreen", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "close", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setPurchaseDelegate", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setLocale", returnType: CAPPluginReturnPromise),
Expand Down Expand Up @@ -83,6 +84,30 @@ public class NoCodesPlugin: CAPPlugin, CAPBridgedPlugin {
call.resolve()
}

@objc func loadScreen(_ call: CAPPluginCall) {
guard let contextKey = call.getString("contextKey") else {
return call.noNecessaryDataError()
}

DispatchQueue.main.async { [weak self] in
guard let noCodesSandwich = self?.noCodesSandwich else {
return call.reject("No-Codes SDK is not initialized", "SDKInitializationError")
}

noCodesSandwich.loadScreen(contextKey) { data, error in
if let error = error {
return call.sandwichError(error)
}

guard let data else {
return call.reject("Failed to load No-Code screen: empty native response", "ScreenLoadingFailed")
}

call.resolve(data)
}
}
}

@objc func close(_ call: CAPPluginCall) {
DispatchQueue.main.async { [weak self] in
self?.noCodesSandwich?.close()
Expand Down
14 changes: 14 additions & 0 deletions src/NoCodesApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { NoCodesScreen } from './dto/NoCodesScreen';
import type { PurchaseDelegate } from './dto/PurchaseDelegate';
import { ScreenPresentationConfig } from './dto/ScreenPresentationConfig';
import { NoCodesTheme } from './dto/enums';
Expand Down Expand Up @@ -28,6 +29,19 @@ export interface NoCodesApi {
*/
showScreen(contextKey: string, customVariables?: Record<string, string>): void;

/**
* 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 {@link showScreen} — the loaded content is served from cache.
*
* The returned {@link NoCodesScreen} exposes the typed default variables configured
* in the builder and the default selected product id.
*
* @param contextKey the context key of the screen to load.
* @returns the loaded screen data.
*/
loadScreen(contextKey: string): Promise<NoCodesScreen>;

/**
* Close the current opened No-Code screen.
*/
Expand Down
6 changes: 4 additions & 2 deletions src/NoCodesNativePlugin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { registerPlugin } from '@capacitor/core';
import type { QProduct, QNoCodeAction, QNoCodesError, QNoCodeScreenInfo } from './internal/Mapper';
import type { QProduct, QNoCodeAction, QNoCodeCustomActionInfo, QNoCodesError, QNoCodeScreen, QNoCodeScreenInfo } from './internal/Mapper';

export type NoCodeEvent = {
name: string;
payload: QNoCodeAction | QNoCodesError | QNoCodeScreenInfo | undefined;
payload: QNoCodeAction | QNoCodesError | QNoCodeScreenInfo | QNoCodeCustomActionInfo | undefined;
};

export interface NoCodesNativePlugin {
Expand All @@ -23,6 +23,8 @@ export interface NoCodesNativePlugin {

showScreen(params: { contextKey: string; customVariables?: Record<string, string> }): Promise<void>;

loadScreen(params: { contextKey: string }): Promise<QNoCodeScreen>;

close(): Promise<void>;

setPurchaseDelegate(): void;
Expand Down
12 changes: 12 additions & 0 deletions src/dto/NoCodesListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ export interface NoCodesListener {
*/
onActionFinishedExecuting: (action: NoCodesAction) => void;

/**
* 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 {@link NoCodesApi.close} if needed.
*
* Optional to keep existing listener implementations source-compatible.
*
* @param value the string value configured for the custom action in the builder,
* or an empty string if no value was configured
*/
onCustomAction?: (value: string) => void;

/**
* Called when No-Codes flow is finished
*/
Expand Down
110 changes: 110 additions & 0 deletions src/dto/NoCodesScreen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type {NoCodesScreenVariableKind} from './enums';

/**
* 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
* (boolean / string / number) rather than being coerced to a string.
*/
export class NoCodesScreenVariable {
/**
* What the variable represents — see {@link NoCodesScreenVariableKind}.
*/
kind: NoCodesScreenVariableKind;

/**
* Variable name it is addressed by (`variable.<key>` in the builder for custom
* variables, the slot name for product slots). May contain spaces.
*/
key: string;

/**
* Authored value type: `"boolean"`, `"string"` or `"number"`.
*/
type: string;

/**
* The configured default value, preserving its native type.
* Null when no default value was authored.
*/
value: boolean | string | number | null;

/**
* 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.
*/
stringValue: string;

constructor(
kind: NoCodesScreenVariableKind,
key: string,
type: string,
value: boolean | string | number | null,
stringValue: string,
) {
this.kind = kind;
this.key = key;
this.type = type;
this.value = value;
this.stringValue = stringValue;
}
}

/**
* A loaded No-Codes screen returned from {@link NoCodesApi.loadScreen}.
*
* 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
* {@link NoCodesApi.showScreen}.
*/
export class NoCodesScreen {
/**
* Identifier of the screen.
*/
id: string;

/**
* The context key of the screen set in the No-Codes builder.
*/
contextKey: string;

/**
* The Qonversion product id selected by default when the screen opens (the builder's
* Default Product), or undefined when none is configured.
*/
defaultSelectedProductId: string | undefined;

/**
* Typed default variables of the screen configured in the builder: authored custom
* variables and product slots. Read them by {@link NoCodesScreenVariable.key} (may be empty).
*/
defaultVariables: NoCodesScreenVariable[];

constructor(
id: string,
contextKey: string,
defaultSelectedProductId: string | undefined,
defaultVariables: NoCodesScreenVariable[],
) {
this.id = id;
this.contextKey = contextKey;
this.defaultSelectedProductId = defaultSelectedProductId;
this.defaultVariables = defaultVariables;
}

/**
* Returns the default variable configured under the given key, or undefined 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 `kind` 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 {@link defaultSelectedProductId} — it needs no key.
*/
defaultVariable(key: string, kind?: NoCodesScreenVariableKind): NoCodesScreenVariable | undefined {
return this.defaultVariables.find(
variable => variable.key === key && (kind === undefined || variable.kind === kind)
);
}
}
32 changes: 31 additions & 1 deletion src/dto/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,8 @@ export enum NoCodesErrorCode {
PRODUCTS_LOADING_FAILED = "ProductsLoadingFailed", // iOS
RATE_LIMIT_EXCEEDED = "RateLimitExceeded", // iOS
SCREEN_LOADING_FAILED = "ScreenLoadingFailed", // iOS
SDK_INITIALIZATION_ERROR = "SDKInitializationError" // iOS
SDK_INITIALIZATION_ERROR = "SDKInitializationError", // iOS
CLIENT_ERROR = "ClientError"
}

export enum PurchaseResultStatus {
Expand Down Expand Up @@ -443,3 +444,32 @@ export enum NoCodesTheme {
*/
DARK = "dark",
}

/**
* 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 {@link NoCodesScreenVariableKind.UNKNOWN} instead of failing.
*/
export enum NoCodesScreenVariableKind {
/**
* A Screen Variable authored in the builder's Variables section.
*/
CUSTOM = "custom",

/**
* A product slot: the variable key is the slot name and the value is the default
* Qonversion product id assigned to it.
*/
PRODUCT = "product",

/**
* The screen's Default Product configured in the builder: the value is
* the Qonversion product id selected by default.
*/
SELECTED_PRODUCT = "selected_product",

/**
* A kind introduced on the backend after this SDK version was released.
*/
UNKNOWN = "unknown",
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ export type { NoCodesListener } from './dto/NoCodesListener';
export type { PurchaseDelegate } from './dto/PurchaseDelegate';
export { NoCodesAction } from './dto/NoCodesAction';
export { NoCodesError } from './dto/NoCodesError';
export { NoCodesScreen, NoCodesScreenVariable } from './dto/NoCodesScreen';
export { ScreenPresentationConfig } from './dto/ScreenPresentationConfig';
52 changes: 52 additions & 0 deletions src/internal/Mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ExperimentGroupType,
IntroEligibilityStatus,
NoCodesErrorCode,
NoCodesScreenVariableKind,
OfferingTag,
PricingPhaseRecurrenceMode,
PricingPhaseType,
Expand Down Expand Up @@ -54,6 +55,7 @@ import {ProductInstallmentPlanDetails} from '../dto/storeProducts/ProductInstall
import {PromotionalOffer} from '../dto/PromotionalOffer';
import {SKPaymentDiscount} from '../dto/storeProducts/SKPaymentDiscount';
import {NoCodesAction} from '../dto/NoCodesAction';
import {NoCodesScreen, NoCodesScreenVariable} from '../dto/NoCodesScreen';
import {NoCodesError} from '../dto/NoCodesError';
import {ScreenPresentationConfig} from '../dto/ScreenPresentationConfig';

Expand Down Expand Up @@ -216,6 +218,23 @@ export type QNoCodesError = QQonversionError & {

export type QNoCodeScreenInfo = { screenId: string };

export type QNoCodeCustomActionInfo = { value?: string | null };

export type QScreenVariable = {
kind?: string | null;
key: string;
type: string;
value?: boolean | string | number | null;
stringValue?: string | null;
};

export type QNoCodeScreen = {
id: string;
contextKey: string;
defaultSelectedProductId?: string | null;
defaultVariables?: QScreenVariable[] | null;
};

export type QEntitlement = {
id: string;
productId: string;
Expand Down Expand Up @@ -1119,6 +1138,38 @@ class Mapper {
);
}

static convertScreen(payload: QNoCodeScreen): NoCodesScreen {
const variables = (payload.defaultVariables ?? []).map(
variable =>
new NoCodesScreenVariable(
this.convertScreenVariableKind(variable.kind),
variable.key,
variable.type,
variable.value ?? null,
variable.stringValue ?? ''
)
);
return new NoCodesScreen(
payload.id,
payload.contextKey,
payload.defaultSelectedProductId ?? undefined,
variables
);
}

static convertScreenVariableKind(kind: string | null | undefined): NoCodesScreenVariableKind {
switch (kind) {
case NoCodesScreenVariableKind.CUSTOM:
return NoCodesScreenVariableKind.CUSTOM;
case NoCodesScreenVariableKind.PRODUCT:
return NoCodesScreenVariableKind.PRODUCT;
case NoCodesScreenVariableKind.SELECTED_PRODUCT:
return NoCodesScreenVariableKind.SELECTED_PRODUCT;
default:
return NoCodesScreenVariableKind.UNKNOWN;
}
}

static convertNoCodesError(payload: QNoCodesError | undefined): NoCodesError | undefined {
if (!payload) return undefined;

Expand Down Expand Up @@ -1161,6 +1212,7 @@ class Mapper {
case NoCodesErrorCode.RATE_LIMIT_EXCEEDED: return NoCodesErrorCode.RATE_LIMIT_EXCEEDED;
case NoCodesErrorCode.SCREEN_LOADING_FAILED: return NoCodesErrorCode.SCREEN_LOADING_FAILED;
case NoCodesErrorCode.SDK_INITIALIZATION_ERROR: return NoCodesErrorCode.SDK_INITIALIZATION_ERROR;
case NoCodesErrorCode.CLIENT_ERROR: return NoCodesErrorCode.CLIENT_ERROR;
}

return NoCodesErrorCode.UNKNOWN;
Expand Down
Loading
Loading