Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidKotlinMultiplatformLibrary)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ interface ConstellationSdk {
*/
fun createCase(caseClassName: String, startingFields: Map<String, Any> = emptyMap())

/**
* Opens an assignment with the given ID.
*
* @param assignmentId id of the assignment to be opened
*/
fun openAssignment(assignmentId: String)

companion object {
/**
* Allows to create [ConstellationSdk] object.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.pega.constellation.sdk.kmp.core

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive

/**
* Represents various actions that can be performed using the Constellation SDK.
*/
@Serializable(with = ConstellationSdkKActionSerializer::class)
sealed class ConstellationSdkAction {

data class CreateCase(
val caseClassName: String,
val startingFields: JsonObject? = null
) : ConstellationSdkAction()

data class OpenAssignment(
val assignmentId: String
) : ConstellationSdkAction()
}


object ConstellationSdkKActionSerializer : KSerializer<ConstellationSdkAction> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most probably this custom serializer is not needed as kotlin should handle serialization of sealed classes out of the box. You just need to add @Serialiable to sealed class and subclasses and use type as a class discriminator instead of actionType.

override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ConstellationSdkAction")

override fun serialize(encoder: Encoder, value: ConstellationSdkAction) {
val jsonEncoder = encoder as? JsonEncoder
?: throw SerializationException("This class can be serialized only by JSON")

val jsonObject = when (value) {
is ConstellationSdkAction.CreateCase -> {
buildJsonObject {
put("actionType", JsonPrimitive("CreateCase"))
put("caseClassName", JsonPrimitive(value.caseClassName))
value.startingFields?.let { put("startingFields", it) }
}
}
is ConstellationSdkAction.OpenAssignment -> {
buildJsonObject {
put("actionType", JsonPrimitive("OpenAssignment"))
put("assignmentId", JsonPrimitive(value.assignmentId))
}
}
}
jsonEncoder.encodeJsonElement(jsonObject)
}

override fun deserialize(decoder: Decoder): ConstellationSdkAction {
val jsonDecoder = decoder as? JsonDecoder
?: throw SerializationException("This class can be deserialized only by JSON")

val json = jsonDecoder.decodeJsonElement().jsonObject
val actionType = json["actionType"]?.jsonPrimitive?.content
?: throw SerializationException("Missing 'actionType' field")

return when (actionType) {
"CreateCase" -> {
val caseClassName = json["caseClassName"]?.jsonPrimitive?.content
?: throw SerializationException("Missing 'caseClassName'")
val startingFields = json["startingFields"]?.jsonObject
ConstellationSdkAction.CreateCase(caseClassName, startingFields)
}
"OpenAssignment" -> {
val assignmentId = json["assignmentId"]?.jsonPrimitive?.content
?: throw SerializationException("Missing 'assignmentId'")
ConstellationSdkAction.OpenAssignment(assignmentId)
}
else -> throw SerializationException("Unknown actionType: $actionType")
}
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
package com.pega.constellation.sdk.kmp.core

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class EngineConfiguration(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be implementation-detail of specific engine. I'd move it to engine-webview > commonMain and make it internal, as it is not something to be used by SDK clients. Btw - commonMain sourceset was recently created here: https://github.com/pegasystems/constellation-mobile-sdk/pull/49/files#diff-2398e22a8cacbeea18a4b5885f734cb50ef9029a687327c36481db76d377e40a

val url: String,
val version: String,
val action: ConstellationSdkAction,
val debuggable: Boolean
) {
fun toJsonString(): String = Json.encodeToString(this)
}

/**
* Constellation SDK Engine that orchestrates Pega application logic.
Expand All @@ -12,9 +24,9 @@ interface ConstellationSdkEngine {
fun configure(config: ConstellationSdkConfig, handler: EngineEventHandler)

/**
* Creates a case of the specified class name with the provided starting fields.
* Performs the specified action using the Constellation SDK.
*/
fun createCase(caseClassName: String, startingFields: Map<String, Any>)
fun performAction(action: ConstellationSdkAction)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.pega.constellation.sdk.kmp.core.internal

import com.pega.constellation.sdk.kmp.core.ConstellationSdkAction
import com.pega.constellation.sdk.kmp.core.ConstellationSdk
import com.pega.constellation.sdk.kmp.core.ConstellationSdk.State
import com.pega.constellation.sdk.kmp.core.ConstellationSdkConfig
Expand All @@ -8,6 +9,7 @@ import com.pega.constellation.sdk.kmp.core.EngineEvent
import com.pega.constellation.sdk.kmp.core.components.containers.RootContainerComponent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.json.JsonObject

internal class ConstellationSdkImpl(
config: ConstellationSdkConfig,
Expand All @@ -23,7 +25,14 @@ internal class ConstellationSdkImpl(
}

override fun createCase(caseClassName: String, startingFields: Map<String, Any>) {
engine.createCase(caseClassName, startingFields)
engine.performAction(ConstellationSdkAction.CreateCase(
caseClassName,
startingFields.toJsonElement() as? JsonObject)
)
}

override fun openAssignment(assignmentId: String) {
engine.performAction(ConstellationSdkAction.OpenAssignment(assignmentId))
}

private fun onEngineEvent(event: EngineEvent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.pega.constellation.sdk.kmp.core.internal

import com.pega.constellation.sdk.kmp.core.Log
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive

private const val TAG = "JsonHelper"

fun Any.toJsonElement(): JsonElement? = when (this) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be internal

is String -> JsonPrimitive(this)
is Int -> JsonPrimitive(this)
is Double -> JsonPrimitive(this)
is Float -> JsonPrimitive(this)
is Boolean -> JsonPrimitive(this)
is List<*> -> {
val jsonElements = this.mapNotNull {
it?.toJsonElement() ?: run {
val type = it?.let { it::class } ?: "null"
Log.w(TAG, "Unsupported type $type, cannot convert to JsonElement.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't it result in duplicated logs for the same type? It seems that the same log will be printed from else branch (as we're calling toJsonElement recursively). I think we could get rid of that run {} block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point - will refactor it

null
}
}
JsonArray(jsonElements)
}
is Map<*, *> -> {
val map = this.entries.mapNotNull { (key, value) ->
(key as? String)?.let { k ->
value?.toJsonElement()?.let { v -> k to v }
} ?: run {
val keyType = key?.let { it::class } ?: "null"
val valueType = value?.let { it::class } ?: "null"
Log.w(TAG, "Unsupported key type <$keyType> or value type <$valueType>, cannot convert to JsonElement.")
null
}
}.toMap()
JsonObject(map)
}
else -> {
Log.w(TAG, "Unsupported type ${this::class}, cannot convert to JsonElement.")
null
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.pega.constellation.sdk.kmp.engine.mock

import com.pega.constellation.sdk.kmp.core.ConstellationSdkAction
import com.pega.constellation.sdk.kmp.core.ConstellationSdkConfig
import com.pega.constellation.sdk.kmp.core.ConstellationSdkEngine
import com.pega.constellation.sdk.kmp.core.EngineEvent
Expand Down Expand Up @@ -32,7 +33,7 @@ class MockSdkEngine : ConstellationSdkEngine {
this.handler = handler
}

override fun createCase(caseClassName: String, startingFields: Map<String, Any>) {
override fun performAction(action: ConstellationSdkAction) {
handler.handle(EngineEvent.Loading)
config.componentManager.configureComponents()
handler.handle(EngineEvent.Ready)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import android.util.Log
import android.webkit.WebView
import android.webkit.WebView.setWebContentsDebuggingEnabled
import android.webkit.WebViewClient
import com.pega.constellation.sdk.kmp.core.ConstellationSdkAction
import com.pega.constellation.sdk.kmp.core.ConstellationSdkConfig
import com.pega.constellation.sdk.kmp.core.ConstellationSdkEngine
import com.pega.constellation.sdk.kmp.core.EngineConfiguration
import com.pega.constellation.sdk.kmp.core.EngineError
import com.pega.constellation.sdk.kmp.core.EngineEvent
import com.pega.constellation.sdk.kmp.core.EngineEventHandler
Expand Down Expand Up @@ -67,20 +69,21 @@ class AndroidWebViewEngine(
this.webView = createWebView(webViewClient)
}

override fun createCase(caseClassName: String, startingFields: Map<String, Any>) {
override fun performAction(action: ConstellationSdkAction) {
handler.handle(EngineEvent.Loading)
webViewClient.onPageLoad = { onPageLoad(caseClassName, startingFields) }
webViewClient.onPageLoad = { onPageLoad(action) }
webView.loadUrl(config.pegaUrl)
}

private fun onPageLoad(caseClassName: String, startingFields: Map<String, Any>) {
private fun onPageLoad(action: ConstellationSdkAction) {
val configuration = EngineConfiguration(
url = config.pegaUrl,
version = config.pegaVersion,
action = action,
debuggable = config.debuggable
)
evaluateInit(
sdkConfig = JSONObject().apply {
put("url", config.pegaUrl)
put("version", config.pegaVersion)
put("caseClassName", caseClassName)
put("startingFields", JSONObject(startingFields))
}.toString(),
sdkConfig = configuration.toJsonString(),
scripts = componentManager.getCustomComponentDefinitions()
.filter { it.script != null }
.associate { it.type.type to it.script!!.assetPath(context) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.pega.constellation.sdk.kmp.engine.webview.ios

import PegaMobileWKWebViewTweaks.allowForHTTPSchemeHandlerRegistration
import PegaMobileWKWebViewTweaks.applyTweaks
import com.pega.constellation.sdk.kmp.core.ConstellationSdkAction
import com.pega.constellation.sdk.kmp.core.ConstellationSdkConfig
import com.pega.constellation.sdk.kmp.core.ConstellationSdkEngine
import com.pega.constellation.sdk.kmp.core.EngineConfiguration
import com.pega.constellation.sdk.kmp.core.EngineEvent
import com.pega.constellation.sdk.kmp.core.EngineEventHandler
import com.pega.constellation.sdk.kmp.core.Log
Expand All @@ -18,7 +20,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
Expand All @@ -41,20 +42,6 @@ data class ComponentEvent(
val eventContent: String
)

@Serializable
data class EngineConfiguration(
val url: String,
val version: String,
val caseClassName: String? = null,
val debuggable: Boolean
) {

fun toJsonString(): String =
Json.encodeToString(this)
.replace("\n", " ")

}

@OptIn(ExperimentalForeignApi::class)
class WKWebViewBasedEngine(
val provider: ResourceProvider
Expand Down Expand Up @@ -98,10 +85,7 @@ class WKWebViewBasedEngine(
this.resourceHandler.delegate = resourceProviderManager
}

override fun createCase(
caseClassName: String,
startingFields: Map<String, Any>
) {
override fun performAction(action: ConstellationSdkAction) {
mainScope?.cancel()
mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())

Expand All @@ -110,7 +94,7 @@ class WKWebViewBasedEngine(
val engineConfig = EngineConfiguration(
url = config.pegaUrl,
version = config.pegaVersion,
caseClassName = caseClassName,
action = action,
debuggable = config.debuggable
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ final class SDKWrapperTests: XCTestCase {
}

private class MockedSDK: ConstellationSdk {
func openAssignment(assignmentId: String) {
XCTFail("not implemented")
}

func createCase(caseClassName: String, startingFields: [String : Any]) {
XCTFail("not implemented")
Expand Down
12 changes: 10 additions & 2 deletions scripts/init/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { initPlatforms } from './init-platforms.js';
import { initialRender } from './initial-render.js';
import { bootstrap } from './bootstrap.js'
import { createCase } from './create-case.js';
import { openAssignment } from './open-assignment.js';
import { getSdkComponentMap } from '../dxcomponents/mappings/sdk-component-map.js';
import { bridge } from '../bridge/native-bridge.js';
import { subscribeForEvents } from './init-events.js';
Expand All @@ -17,11 +18,18 @@ async function init(sdkConfig, componentsOverridesStr) {
initPlatforms(componentsOverridesStr);
const config = JSON.parse(sdkConfig);
await bootstrap(config.url, config.version, onPCoreReady);
await createCase(config.caseClassName, config.startingFields);
if (config.action.actionType === "CreateCase") {
await createCase(config.action.caseClassName, config.action.startingFields);
} else if (config.action.actionType === "OpenAssignment") {
await openAssignment(config.action.assignmentId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it is okay for now, but in the future we should consider making it possible to "interact" with the application/engine instead of just reinitializing everything each time we want to open or create something.

} else {
const errorMessage = "Unknown action type: " + config.action.actionType;
throw new Error(errorMessage);
}
console.log(TAG, "Constellation SDK initialization completed");
bridge.onReady();
} catch (error) {
const errorMessage = "Constellation SDK initialization failed! " + (error?.message ?? "")
const errorMessage = "Constellation SDK initialization failed! " + (error?.message ?? "");
console.error(errorMessage);
bridge.onError("InitError", errorMessage);
}
Expand Down
9 changes: 9 additions & 0 deletions scripts/init/open-assignment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export async function openAssignment(assignmentId) {

const options = {
pageName: 'pyEmbedAssignment'
};

console.log("[OpenAssignment]", "Opening assignment: " + assignmentId);
await PCore.getMashupApi().openAssignment(assignmentId, PCore.getConstants().APP.APP, options);
Copy link
Contributor

@lukaszgajewski-pega lukaszgajewski-pega Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, that's so simple 😎

}