mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
Merge pull request #1960 from selfxyz/release/staging-2026-04-10
Release to Staging v2.9.17 - 2026-04-10
This commit is contained in:
@@ -199,18 +199,18 @@ library Formatter {
|
||||
return bytesArray;
|
||||
}
|
||||
|
||||
function fieldElementsToBytesKyc(uint256[11] memory publicSignals) internal pure returns (bytes memory) {
|
||||
for (uint256 i = 0; i < 11; i++) {
|
||||
function fieldElementsToBytesKyc(uint256[10] memory publicSignals) internal pure returns (bytes memory) {
|
||||
for (uint256 i = 0; i < 10; i++) {
|
||||
if (publicSignals[i] >= SNARK_SCALAR_FIELD) {
|
||||
revert InvalidFieldElement();
|
||||
}
|
||||
}
|
||||
|
||||
uint8[11] memory bytesCount = [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 25];
|
||||
bytes memory bytesArray = new bytes(335);
|
||||
uint8[10] memory bytesCount = [31, 31, 31, 31, 31, 31, 31, 31, 31, 19];
|
||||
bytes memory bytesArray = new bytes(298);
|
||||
|
||||
uint256 index = 0;
|
||||
for (uint256 i = 0; i < 11; i++) {
|
||||
for (uint256 i = 0; i < 10; i++) {
|
||||
uint256 element = publicSignals[i];
|
||||
for (uint8 j = 0; j < bytesCount[i]; j++) {
|
||||
bytesArray[index++] = bytes1(uint8(element & 0xff));
|
||||
|
||||
@@ -109,6 +109,13 @@ library OutputFormatterLib {
|
||||
}
|
||||
aadhaarOutput.revealedDataPacked = Formatter.fieldElementsToBytesAadhaar(revealedDataPacked);
|
||||
|
||||
// Extract forbidden countries list
|
||||
for (uint256 i = 0; i < 4; i++) {
|
||||
aadhaarOutput.forbiddenCountriesListPacked[i] = vcAndDiscloseProof.pubSignals[
|
||||
indices.forbiddenCountriesListPackedIndex + i
|
||||
];
|
||||
}
|
||||
|
||||
return abi.encode(aadhaarOutput);
|
||||
}
|
||||
|
||||
@@ -131,12 +138,19 @@ library OutputFormatterLib {
|
||||
kycOutput.userIdentifier = userIdentifier;
|
||||
kycOutput.nullifier = vcAndDiscloseProof.pubSignals[indices.nullifierIndex];
|
||||
|
||||
uint256[11] memory revealedDataPacked;
|
||||
for (uint256 i = 0; i < 11; i++) {
|
||||
uint256[10] memory revealedDataPacked;
|
||||
for (uint256 i = 0; i < 10; i++) {
|
||||
revealedDataPacked[i] = vcAndDiscloseProof.pubSignals[indices.revealedDataPackedIndex + i];
|
||||
}
|
||||
kycOutput.revealedDataPacked = Formatter.fieldElementsToBytesKyc(revealedDataPacked);
|
||||
|
||||
// Extract forbidden countries list
|
||||
for (uint256 i = 0; i < 4; i++) {
|
||||
kycOutput.forbiddenCountriesListPacked[i] = vcAndDiscloseProof.pubSignals[
|
||||
indices.forbiddenCountriesListPackedIndex + i
|
||||
];
|
||||
}
|
||||
|
||||
return abi.encode(kycOutput);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,63 @@ describe("Self Verification Flow V2 - KYC", () => {
|
||||
expect(actualUserData).to.equal(expectedUserData);
|
||||
});
|
||||
|
||||
it("should complete full KYC verification flow with non-empty forbidden countries", async () => {
|
||||
const destChainId = 31337;
|
||||
const user1Address = await deployedActors.user1.getAddress();
|
||||
const userData = "test-user-data-for-verification";
|
||||
|
||||
const fcListStrings = ["IRN", "PRK", "RUS"];
|
||||
|
||||
// Reuse the same tree so the merkle root matches the on-chain registry
|
||||
const fcTestInputs = generateKycDiscloseInputFromDummy(
|
||||
false,
|
||||
nameAndDob_smt,
|
||||
nameAndYob_smt,
|
||||
tree,
|
||||
false,
|
||||
scopeAsBigInt.toString(),
|
||||
userIdentifierHash.toString(),
|
||||
["GENDER", "FULL_NAME", "DOB", "ID_NUMBER", "ISSUANCE_DATE", "EXPIRY_DATE", "COUNTRY", "GENDER", "ADDRESS"],
|
||||
fcListStrings,
|
||||
0,
|
||||
false,
|
||||
KYC_ATTESTATION_ID,
|
||||
);
|
||||
|
||||
const fcPacked = getPackedForbiddenCountries(fcListStrings as any);
|
||||
const fcConfig = {
|
||||
olderThanEnabled: true,
|
||||
olderThan: "00",
|
||||
forbiddenCountriesEnabled: true,
|
||||
forbiddenCountriesListPacked: fcPacked as [BigNumberish, BigNumberish, BigNumberish, BigNumberish],
|
||||
ofacEnabled: [false, false, false] as [boolean, boolean, boolean],
|
||||
};
|
||||
|
||||
await deployedActors.testSelfVerificationRoot.setVerificationConfig(fcConfig);
|
||||
|
||||
const fcProof = await generateVcAndDiscloseKycProof(fcTestInputs);
|
||||
|
||||
const destChainIdHex = ethers.zeroPadValue(ethers.toBeHex(destChainId), 32);
|
||||
const userContextData = ethers.solidityPacked(
|
||||
["bytes32", "bytes32", "bytes"],
|
||||
[destChainIdHex, ethers.zeroPadValue(user1Address, 32), ethers.toUtf8Bytes(userData)],
|
||||
);
|
||||
|
||||
const attestationId = ethers.zeroPadValue(ethers.toBeHex(BigInt(KYC_ATTESTATION_ID)), 32);
|
||||
const encodedProof = ethers.AbiCoder.defaultAbiCoder().encode(
|
||||
["tuple(uint256[2] a, uint256[2][2] b, uint256[2] c, uint256[] pubSignals)"],
|
||||
[[fcProof.a, fcProof.b, fcProof.c, fcProof.pubSignals]],
|
||||
);
|
||||
const proofData = ethers.solidityPacked(["bytes32", "bytes"], [attestationId, encodedProof]);
|
||||
|
||||
await deployedActors.testSelfVerificationRoot.resetTestState();
|
||||
|
||||
const tx = await deployedActors.testSelfVerificationRoot.verifySelfProof(proofData, userContextData);
|
||||
|
||||
await expect(tx).to.emit(deployedActors.testSelfVerificationRoot, "VerificationCompleted");
|
||||
expect(await deployedActors.testSelfVerificationRoot.verificationSuccessful()).to.be.true;
|
||||
});
|
||||
|
||||
it("should not verify if the config is not set", async () => {
|
||||
const destChainId = ethers.zeroPadValue(ethers.toBeHex(31337), 32);
|
||||
const user1Address = await deployedActors.user1.getAddress();
|
||||
|
||||
@@ -11,7 +11,6 @@ import SelfSdkSwift
|
||||
// exported from the ComposeApp (KMP) framework.
|
||||
|
||||
extension SecureStorageProviderImpl: SecureStorageProvider {}
|
||||
extension CryptoProviderImpl: CryptoProvider {}
|
||||
|
||||
@main
|
||||
struct iOSApp: App {
|
||||
@@ -19,7 +18,6 @@ struct iOSApp: App {
|
||||
// Register all Swift provider implementations with the KMP SdkProviderRegistry
|
||||
let registry = SdkProviderRegistry.shared
|
||||
registry.secureStorage = SecureStorageProviderImpl()
|
||||
registry.crypto = CryptoProviderImpl()
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import xyz.self.sdk.api.SelfSdk
|
||||
import xyz.self.sdk.providers.AndroidKeystoreCryptoProvider
|
||||
import xyz.self.sdk.providers.EncryptedSharedPreferencesProvider
|
||||
import xyz.self.sdk.providers.SdkProviderRegistry
|
||||
|
||||
@@ -21,10 +20,6 @@ class MainActivity : ComponentActivity() {
|
||||
if (SdkProviderRegistry.secureStorage == null) {
|
||||
SdkProviderRegistry.secureStorage = EncryptedSharedPreferencesProvider(this)
|
||||
}
|
||||
if (SdkProviderRegistry.crypto == null) {
|
||||
SdkProviderRegistry.crypto = AndroidKeystoreCryptoProvider()
|
||||
}
|
||||
|
||||
SelfSdk.bindActivity(this)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
|
||||
@@ -52,7 +52,6 @@ data class SmokeResult(
|
||||
@Composable
|
||||
fun DomainSmokeScreen(navController: NavController) {
|
||||
var storageResult by remember { mutableStateOf(SmokeResult()) }
|
||||
var cryptoResult by remember { mutableStateOf(SmokeResult()) }
|
||||
var running by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -69,7 +68,7 @@ fun DomainSmokeScreen(navController: NavController) {
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Tests secureStorage, crypto, and lifecycle providers directly.",
|
||||
text = "Tests secureStorage and lifecycle providers directly.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
@@ -77,10 +76,8 @@ fun DomainSmokeScreen(navController: NavController) {
|
||||
onClick = {
|
||||
running = true
|
||||
storageResult = SmokeResult()
|
||||
cryptoResult = SmokeResult()
|
||||
scope.launch {
|
||||
storageResult = runStorageSmoke()
|
||||
cryptoResult = runCryptoSmoke()
|
||||
running = false
|
||||
}
|
||||
},
|
||||
@@ -91,7 +88,6 @@ fun DomainSmokeScreen(navController: NavController) {
|
||||
}
|
||||
|
||||
SmokeCard("secureStorage", storageResult)
|
||||
SmokeCard("crypto", cryptoResult)
|
||||
SmokeCard("lifecycle", SmokeResult(CheckStatus.PASS, "ready/setResult validated via SDK launch flow"))
|
||||
|
||||
Text(
|
||||
@@ -186,34 +182,3 @@ private fun runStorageSmoke(): SmokeResult {
|
||||
SmokeResult(CheckStatus.FAIL, "Exception: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCryptoSmoke(): SmokeResult {
|
||||
val provider =
|
||||
SdkProviderRegistry.crypto
|
||||
?: return SmokeResult(CheckStatus.FAIL, "Provider not configured")
|
||||
return try {
|
||||
val keyRef = "smoke_test_key_${kotlin.random.Random.nextInt(100000)}"
|
||||
|
||||
provider.generateKey(keyRef)
|
||||
|
||||
val publicKey = provider.getPublicKey(keyRef)
|
||||
if (publicKey.isNullOrEmpty()) {
|
||||
return SmokeResult(CheckStatus.FAIL, "getPublicKey returned null/empty")
|
||||
}
|
||||
|
||||
val testData = "dGVzdA==" // base64("test")
|
||||
val signature = provider.sign(keyRef, testData)
|
||||
if (signature.isNullOrEmpty()) {
|
||||
return SmokeResult(CheckStatus.FAIL, "sign returned null/empty")
|
||||
}
|
||||
|
||||
provider.deleteKey(keyRef)
|
||||
|
||||
SmokeResult(
|
||||
CheckStatus.PASS,
|
||||
"generateKey/getPublicKey/sign/deleteKey OK\npubKey=${publicKey.take(20)}...\nsig=${signature.take(20)}...",
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
SmokeResult(CheckStatus.FAIL, "Exception: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import SelfSdkSwift
|
||||
|
||||
// MARK: - Protocol conformance for required providers
|
||||
extension SecureStorageProviderImpl: SecureStorageProvider {}
|
||||
extension CryptoProviderImpl: CryptoProvider {}
|
||||
extension WebViewProviderImpl: WebViewProvider {}
|
||||
|
||||
@main
|
||||
@@ -16,7 +15,6 @@ struct iOSApp: App {
|
||||
init() {
|
||||
// Register only the 3-domain required providers
|
||||
SdkProviderRegistry.shared.secureStorage = SecureStorageProviderImpl()
|
||||
SdkProviderRegistry.shared.crypto = CryptoProviderImpl()
|
||||
IosProviderRegistry.shared.webView = WebViewProviderImpl()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// swift-tools-version:5.9
|
||||
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
// swift-tools-version:5.9
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
|
||||
@@ -121,12 +121,8 @@ actual class SelfSdk private constructor(
|
||||
// Create intent for SelfVerificationActivity
|
||||
val intent =
|
||||
Intent(activity, SelfVerificationActivity::class.java).apply {
|
||||
putExtra(SelfVerificationActivity.EXTRA_DEBUG_MODE, config.debug)
|
||||
putExtra(SelfVerificationActivity.EXTRA_VERIFICATION_REQUEST, serializeRequest(request))
|
||||
putExtra(SelfVerificationActivity.EXTRA_CONFIG, serializeConfig(config))
|
||||
if (config.devServerUrl != null) {
|
||||
putExtra(SelfVerificationActivity.EXTRA_DEV_SERVER_URL, config.devServerUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// Launch the verification activity
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.security.KeyStore
|
||||
import java.security.Signature
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
|
||||
class AndroidKeystoreCryptoProvider : CryptoProvider {
|
||||
internal class AndroidKeystoreCryptoProvider : CryptoProvider {
|
||||
private val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||
|
||||
override fun generateKey(keyRef: String) {
|
||||
|
||||
@@ -23,13 +23,15 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.webkit.WebMessageCompat
|
||||
import androidx.webkit.WebViewCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import xyz.self.sdk.api.SdkConstants
|
||||
import xyz.self.sdk.bridge.MessageRouter
|
||||
|
||||
class AndroidWebViewHost(
|
||||
private val context: Context,
|
||||
private val router: MessageRouter,
|
||||
private val isDebugMode: Boolean = false,
|
||||
private val remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app",
|
||||
private val isDebuggable: Boolean = false,
|
||||
private val remoteWebAppBaseUrl: String = SdkConstants.DEFAULT_REMOTE_WEB_APP_BASE_URL,
|
||||
private val devServerUrl: String? = null,
|
||||
) {
|
||||
private lateinit var webView: WebView
|
||||
@@ -38,6 +40,7 @@ class AndroidWebViewHost(
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun createWebView(queryParams: String = ""): WebView {
|
||||
val effectiveDebug = isDebugMode && isDebuggable
|
||||
webView =
|
||||
WebView(context).apply {
|
||||
settings.apply {
|
||||
@@ -47,7 +50,7 @@ class AndroidWebViewHost(
|
||||
allowContentAccess = false
|
||||
mediaPlaybackRequiresUserGesture = false
|
||||
|
||||
if (isDebugMode) {
|
||||
if (effectiveDebug) {
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
}
|
||||
}
|
||||
@@ -57,7 +60,7 @@ class AndroidWebViewHost(
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): Boolean = !isAllowedNavigationUrl(request?.url?.toString(), isDebugMode, remoteWebAppBaseUrl, devServerUrl)
|
||||
): Boolean = !isAllowedNavigationUrl(request?.url?.toString(), effectiveDebug, remoteWebAppBaseUrl, devServerUrl)
|
||||
|
||||
override fun onReceivedSslError(
|
||||
view: WebView?,
|
||||
@@ -77,7 +80,7 @@ class AndroidWebViewHost(
|
||||
request.deny()
|
||||
return
|
||||
}
|
||||
if (!isTrustedPermissionOrigin(origin.toString(), isDebugMode, remoteWebAppBaseUrl, devServerUrl)) {
|
||||
if (!isTrustedPermissionOrigin(origin.toString(), effectiveDebug, remoteWebAppBaseUrl, devServerUrl)) {
|
||||
request.deny()
|
||||
return
|
||||
}
|
||||
@@ -148,9 +151,9 @@ class AndroidWebViewHost(
|
||||
}
|
||||
}
|
||||
|
||||
installBridge(webView = this)
|
||||
installBridge(webView = this, effectiveDebug = effectiveDebug)
|
||||
|
||||
loadUrl(initialContentUrl(queryParams, isDebugMode, remoteWebAppBaseUrl, devServerUrl))
|
||||
loadUrl(initialContentUrl(queryParams, effectiveDebug, remoteWebAppBaseUrl, devServerUrl))
|
||||
}
|
||||
return webView
|
||||
}
|
||||
@@ -165,7 +168,10 @@ class AndroidWebViewHost(
|
||||
webView.destroy()
|
||||
}
|
||||
|
||||
private fun installBridge(webView: WebView) {
|
||||
private fun installBridge(
|
||||
webView: WebView,
|
||||
effectiveDebug: Boolean,
|
||||
) {
|
||||
check(WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
|
||||
"WEB_MESSAGE_LISTENER not supported — native bridge unavailable on this device"
|
||||
}
|
||||
@@ -173,7 +179,7 @@ class AndroidWebViewHost(
|
||||
WebViewCompat.addWebMessageListener(
|
||||
webView,
|
||||
"SelfNativeAndroid",
|
||||
buildAllowedOriginRules(isDebugMode, remoteWebAppBaseUrl, devServerUrl),
|
||||
buildAllowedOriginRules(effectiveDebug, remoteWebAppBaseUrl, devServerUrl),
|
||||
) { _, message: WebMessageCompat, sourceOrigin, isMainFrame, _ ->
|
||||
if (!isMainFrame) {
|
||||
return@addWebMessageListener
|
||||
@@ -182,7 +188,7 @@ class AndroidWebViewHost(
|
||||
val rawJson = message.data ?: return@addWebMessageListener
|
||||
router.onMessageReceived(
|
||||
rawJson = rawJson,
|
||||
isTrustedSource = isTrustedBridgeOrigin(sourceOrigin.toString(), isDebugMode, remoteWebAppBaseUrl, devServerUrl),
|
||||
isTrustedSource = isTrustedBridgeOrigin(sourceOrigin.toString(), effectiveDebug, remoteWebAppBaseUrl, devServerUrl),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -190,21 +196,17 @@ class AndroidWebViewHost(
|
||||
companion object {
|
||||
const val FILE_CHOOSER_REQUEST_CODE = 1001
|
||||
const val CAMERA_PERMISSION_REQUEST_CODE = 1002
|
||||
private const val BUNDLED_TOUR_PATH = "/tunnel/tour/1"
|
||||
private const val DEBUG_HOST = "127.0.0.1"
|
||||
private const val DEBUG_PORT = 5173
|
||||
private const val DIDIT_HOST = "verify.didit.me"
|
||||
|
||||
internal fun initialContentUrl(
|
||||
queryParams: String,
|
||||
isDebugMode: Boolean,
|
||||
remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app",
|
||||
remoteWebAppBaseUrl: String = SdkConstants.DEFAULT_REMOTE_WEB_APP_BASE_URL,
|
||||
devServerUrl: String? = null,
|
||||
): String {
|
||||
val baseUrl =
|
||||
when {
|
||||
isDebugMode && devServerUrl != null -> devServerUrl.trimEnd('/')
|
||||
isDebugMode -> "http://$DEBUG_HOST:$DEBUG_PORT"
|
||||
isDebugMode -> "http://${SdkConstants.LOOPBACK_HOST}:${SdkConstants.DEBUG_PORT}"
|
||||
else -> {
|
||||
require(remoteWebAppBaseUrl.startsWith("https://")) {
|
||||
"remoteWebAppBaseUrl must use HTTPS in release builds"
|
||||
@@ -213,7 +215,7 @@ class AndroidWebViewHost(
|
||||
}
|
||||
}
|
||||
return buildString {
|
||||
append(baseUrl).append(BUNDLED_TOUR_PATH)
|
||||
append(baseUrl).append(SdkConstants.BUNDLED_TOUR_PATH)
|
||||
if (queryParams.isNotEmpty()) {
|
||||
append("?").append(queryParams)
|
||||
}
|
||||
@@ -267,12 +269,12 @@ class AndroidWebViewHost(
|
||||
private fun isDiditUrl(rawUrl: String?): Boolean {
|
||||
val port = uriPort(rawUrl)
|
||||
return uriScheme(rawUrl) == "https" &&
|
||||
uriHost(rawUrl) == DIDIT_HOST &&
|
||||
uriHost(rawUrl) == SdkConstants.DIDIT_HOST &&
|
||||
(port == null || port == 443)
|
||||
}
|
||||
|
||||
private fun isDebugLocalUrl(rawUrl: String?): Boolean =
|
||||
uriScheme(rawUrl) == "http" && uriHost(rawUrl) == DEBUG_HOST && uriPort(rawUrl) == DEBUG_PORT
|
||||
uriScheme(rawUrl) == "http" && uriHost(rawUrl) == SdkConstants.LOOPBACK_HOST && uriPort(rawUrl) == SdkConstants.DEBUG_PORT
|
||||
|
||||
private fun isDevServerUrl(
|
||||
rawUrl: String?,
|
||||
@@ -304,7 +306,7 @@ class AndroidWebViewHost(
|
||||
}
|
||||
}
|
||||
if (isDebugMode) {
|
||||
add("http://$DEBUG_HOST:$DEBUG_PORT")
|
||||
add("http://${SdkConstants.LOOPBACK_HOST}:${SdkConstants.DEBUG_PORT}")
|
||||
devServerUrl?.let { parseUri(it) }?.let { dev ->
|
||||
val host = dev.host ?: dev.authority
|
||||
val port = resolvedPort(dev)
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
package xyz.self.sdk.webview
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebChromeClient
|
||||
@@ -15,6 +15,10 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import xyz.self.sdk.api.QueryParamsBuilder
|
||||
import xyz.self.sdk.api.SelfSdkConfig
|
||||
import xyz.self.sdk.api.VerificationRequest
|
||||
import xyz.self.sdk.api.verificationResultJson
|
||||
import xyz.self.sdk.bridge.MessageRouter
|
||||
import xyz.self.sdk.handlers.CryptoBridgeHandler
|
||||
import xyz.self.sdk.handlers.LifecycleBridgeHandler
|
||||
@@ -35,7 +39,37 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun initVerificationFlow() {
|
||||
val isDebugMode = intent.getBooleanExtra(EXTRA_DEBUG_MODE, false)
|
||||
val configJson = intent.getStringExtra(EXTRA_CONFIG) ?: "{}"
|
||||
val requestJson = intent.getStringExtra(EXTRA_VERIFICATION_REQUEST)
|
||||
|
||||
val config =
|
||||
try {
|
||||
verificationResultJson.decodeFromString(SelfSdkConfig.serializer(), configJson)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
val request =
|
||||
if (requestJson != null) {
|
||||
try {
|
||||
verificationResultJson.decodeFromString(VerificationRequest.serializer(), requestJson)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (config == null || request == null) {
|
||||
setResult(
|
||||
RESULT_CODE_ERROR,
|
||||
Intent().apply {
|
||||
putExtra(EXTRA_ERROR_CODE, "INVALID_BOOTSTRAP")
|
||||
putExtra(EXTRA_ERROR_MESSAGE, "Invalid verification request/config payload")
|
||||
},
|
||||
)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Register default providers if consumer hasn't set custom ones
|
||||
if (SdkProviderRegistry.secureStorage == null) {
|
||||
@@ -56,31 +90,11 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
|
||||
registerHandlers()
|
||||
|
||||
// Build query params from VerificationRequest JSON
|
||||
val queryParams = buildQueryParams()
|
||||
if (queryParams == null) {
|
||||
setResult(
|
||||
RESULT_CODE_ERROR,
|
||||
Intent().apply {
|
||||
putExtra(EXTRA_ERROR_CODE, "INVALID_BOOTSTRAP")
|
||||
putExtra(EXTRA_ERROR_MESSAGE, "Invalid verification request/config payload")
|
||||
},
|
||||
)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
val queryParams = QueryParamsBuilder.build(config, request)
|
||||
val isDebuggable = (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||
|
||||
val configJson = intent.getStringExtra(EXTRA_CONFIG) ?: "{}"
|
||||
val remoteWebAppBaseUrl =
|
||||
try {
|
||||
org.json.JSONObject(configJson).optString("remoteWebAppBaseUrl", "https://self-app-alpha.vercel.app")
|
||||
} catch (_: Exception) {
|
||||
"https://self-app-alpha.vercel.app"
|
||||
}
|
||||
|
||||
val devServerUrl = intent.getStringExtra(EXTRA_DEV_SERVER_URL)
|
||||
webViewHost = AndroidWebViewHost(this, router, isDebugMode, remoteWebAppBaseUrl, devServerUrl)
|
||||
val webView = webViewHost.createWebView(queryParams)
|
||||
webViewHost = AndroidWebViewHost(this, router, config.debug, isDebuggable, config.remoteWebAppBaseUrl, config.devServerUrl)
|
||||
val webView = webViewHost.createWebView(queryParams ?: "")
|
||||
val wrapper =
|
||||
FrameLayout(this).apply {
|
||||
addView(
|
||||
@@ -115,63 +129,6 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
router.register(LifecycleBridgeHandler(this))
|
||||
}
|
||||
|
||||
private fun buildQueryParams(): String? {
|
||||
val requestJson = intent.getStringExtra(EXTRA_VERIFICATION_REQUEST) ?: return null
|
||||
val configJson = intent.getStringExtra(EXTRA_CONFIG) ?: "{}"
|
||||
return try {
|
||||
val json = org.json.JSONObject(requestJson)
|
||||
val config = org.json.JSONObject(configJson)
|
||||
buildString {
|
||||
var first = true
|
||||
|
||||
fun append(
|
||||
key: String,
|
||||
value: String?,
|
||||
) {
|
||||
if (value.isNullOrEmpty()) return
|
||||
if (!first) append("&")
|
||||
append("$key=${Uri.encode(value)}")
|
||||
first = false
|
||||
}
|
||||
|
||||
// Config params (always present)
|
||||
val endpoint = config.optString("endpoint", "https://api.self.xyz")
|
||||
append("endpoint", endpoint)
|
||||
val appEndpoint = config.optString("appEndpoint", null)
|
||||
append("appEndpoint", if (appEndpoint.isNullOrEmpty()) endpoint else appEndpoint)
|
||||
append("environment", config.optString("environment", "prod"))
|
||||
append("version", config.optInt("version", 1).toString())
|
||||
|
||||
// Optional config params
|
||||
append("appName", config.optString("appName", null))
|
||||
append("endpointType", config.optString("endpointType", null))
|
||||
val chainID = config.optInt("chainID", 0)
|
||||
if (chainID != 0) append("chainID", chainID.toString())
|
||||
|
||||
// Request params
|
||||
append("verificationId", json.optString("verificationId", null))
|
||||
append("userId", json.optString("userId", null))
|
||||
append("scope", json.optString("scope", null))
|
||||
val disclosures = json.optJSONArray("disclosures")
|
||||
if (disclosures != null && disclosures.length() > 0) {
|
||||
val items = (0 until disclosures.length()).map { disclosures.getString(it) }
|
||||
append("disclosures", items.joinToString(","))
|
||||
}
|
||||
append("resultType", json.optString("resultType", null))
|
||||
val excludedCountries = json.optJSONArray("excludedCountries")
|
||||
if (excludedCountries != null && excludedCountries.length() > 0) {
|
||||
val items = (0 until excludedCountries.length()).map { excludedCountries.getString(it) }
|
||||
append("excludedCountries", items.joinToString(","))
|
||||
}
|
||||
append("userIdType", json.optString("userIdType", null))
|
||||
append("userDefinedData", json.optString("userDefinedData", null))
|
||||
append("selfDefinedData", json.optString("selfDefinedData", null))
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
@@ -218,10 +175,8 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_DEBUG_MODE = "xyz.self.sdk.DEBUG_MODE"
|
||||
const val EXTRA_VERIFICATION_REQUEST = "xyz.self.sdk.VERIFICATION_REQUEST"
|
||||
const val EXTRA_CONFIG = "xyz.self.sdk.CONFIG"
|
||||
const val EXTRA_DEV_SERVER_URL = "xyz.self.sdk.DEV_SERVER_URL"
|
||||
|
||||
const val RESULT_CODE_SUCCESS = RESULT_OK
|
||||
const val RESULT_CODE_ERROR = RESULT_FIRST_USER
|
||||
|
||||
@@ -118,4 +118,34 @@ class AndroidWebViewHostSecurityTest {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isDebugMode true but isDebuggable false loads remote URL`() {
|
||||
// This is the key security test: a release APK with config.debug=true
|
||||
// must NOT load localhost — it should load the remote URL.
|
||||
// The effectiveDebug = isDebugMode && isDebuggable guard ensures this.
|
||||
// When isDebugMode=true but isDebuggable=false, effectiveDebug=false,
|
||||
// so initialContentUrl with isDebugMode=false loads the remote URL.
|
||||
assertEquals(
|
||||
"https://self-app-alpha.vercel.app/tunnel/tour/1",
|
||||
AndroidWebViewHost.initialContentUrl(
|
||||
queryParams = "",
|
||||
isDebugMode = false, // effectiveDebug after isDebugMode && !isDebuggable
|
||||
remoteWebAppBaseUrl = remoteUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isDebugMode true and isDebuggable true loads localhost`() {
|
||||
// Both flags true means we're in a genuine debug build — localhost is allowed.
|
||||
assertEquals(
|
||||
"http://127.0.0.1:5173/tunnel/tour/1",
|
||||
AndroidWebViewHost.initialContentUrl(
|
||||
queryParams = "",
|
||||
isDebugMode = true, // effectiveDebug after isDebugMode && isDebuggable
|
||||
remoteWebAppBaseUrl = remoteUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.sdk.api
|
||||
|
||||
internal object QueryParamsBuilder {
|
||||
fun build(
|
||||
config: SelfSdkConfig,
|
||||
request: VerificationRequest,
|
||||
): String? {
|
||||
val parts = mutableListOf<String>()
|
||||
|
||||
// Config params (always present)
|
||||
parts.add("endpoint=${urlEncode(config.endpoint)}")
|
||||
parts.add("appEndpoint=${urlEncode(config.appEndpoint ?: config.endpoint)}")
|
||||
parts.add("environment=${urlEncode(config.environment.queryValue)}")
|
||||
parts.add("version=${config.version}")
|
||||
|
||||
// Optional config params
|
||||
config.appName?.let { parts.add("appName=${urlEncode(it)}") }
|
||||
config.endpointType?.let { parts.add("endpointType=${urlEncode(it)}") }
|
||||
config.chainID?.let { parts.add("chainID=$it") }
|
||||
|
||||
// Request params
|
||||
request.verificationId?.let { parts.add("verificationId=${urlEncode(it)}") }
|
||||
request.userId?.let { parts.add("userId=${urlEncode(it)}") }
|
||||
request.scope?.let { parts.add("scope=${urlEncode(it)}") }
|
||||
if (request.disclosures.isNotEmpty()) {
|
||||
parts.add("disclosures=${urlEncode(request.disclosures.joinToString(","))}")
|
||||
}
|
||||
request.resultType?.let { parts.add("resultType=${urlEncode(it)}") }
|
||||
if (request.excludedCountries.isNotEmpty()) {
|
||||
parts.add("excludedCountries=${urlEncode(request.excludedCountries.joinToString(","))}")
|
||||
}
|
||||
request.userIdType?.let { parts.add("userIdType=${urlEncode(it)}") }
|
||||
request.userDefinedData?.let { parts.add("userDefinedData=${urlEncode(it)}") }
|
||||
request.selfDefinedData?.let { parts.add("selfDefinedData=${urlEncode(it)}") }
|
||||
|
||||
return parts.joinToString("&").ifEmpty { null }
|
||||
}
|
||||
}
|
||||
|
||||
internal fun urlEncode(value: String): String =
|
||||
value
|
||||
.replace("%", "%25")
|
||||
.replace("&", "%26")
|
||||
.replace("=", "%3D")
|
||||
.replace("+", "%2B")
|
||||
.replace(" ", "%20")
|
||||
.replace("#", "%23")
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.sdk.api
|
||||
|
||||
object SdkConstants {
|
||||
const val LOOPBACK_HOST = "127.0.0.1"
|
||||
const val DEBUG_PORT = 5173
|
||||
const val DIDIT_HOST = "verify.didit.me"
|
||||
const val BUNDLED_TOUR_PATH = "/tunnel/tour/1"
|
||||
const val DEFAULT_ENDPOINT = "https://api.self.xyz"
|
||||
const val DEFAULT_REMOTE_WEB_APP_BASE_URL = "https://self-app-alpha.vercel.app"
|
||||
}
|
||||
@@ -26,7 +26,7 @@ enum class SelfEnvironment {
|
||||
|
||||
@Serializable
|
||||
data class SelfSdkConfig(
|
||||
val endpoint: String = "https://api.self.xyz",
|
||||
val endpoint: String = SdkConstants.DEFAULT_ENDPOINT,
|
||||
val environment: SelfEnvironment = SelfEnvironment.PROD,
|
||||
val debug: Boolean = false,
|
||||
val version: Int = 1,
|
||||
@@ -34,6 +34,6 @@ data class SelfSdkConfig(
|
||||
val appEndpoint: String? = null,
|
||||
val endpointType: String? = null,
|
||||
val chainID: Int? = null,
|
||||
val remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app",
|
||||
val remoteWebAppBaseUrl: String = SdkConstants.DEFAULT_REMOTE_WEB_APP_BASE_URL,
|
||||
val devServerUrl: String? = null,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
package xyz.self.sdk.providers
|
||||
|
||||
interface CryptoProvider {
|
||||
internal interface CryptoProvider {
|
||||
fun generateKey(keyRef: String)
|
||||
|
||||
fun getPublicKey(keyRef: String): String?
|
||||
|
||||
@@ -6,14 +6,13 @@ package xyz.self.sdk.providers
|
||||
|
||||
object SdkProviderRegistry {
|
||||
var secureStorage: SecureStorageProvider? = null
|
||||
var crypto: CryptoProvider? = null
|
||||
internal var crypto: CryptoProvider? = null
|
||||
|
||||
/**
|
||||
* Returns true if the required 3-domain providers are configured.
|
||||
* Only secureStorage and crypto are required — lifecycle is handler-only
|
||||
* with no consumer-provided provider.
|
||||
* Returns true if the required providers are configured.
|
||||
* Only secureStorage is required — crypto is internal and lifecycle is handler-only.
|
||||
*/
|
||||
fun isConfigured(): Boolean = secureStorage != null && crypto != null
|
||||
fun isConfigured(): Boolean = secureStorage != null
|
||||
|
||||
fun reset() {
|
||||
secureStorage = null
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.sdk.api
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class QueryParamsBuilderTest {
|
||||
private val defaultConfig = SelfSdkConfig()
|
||||
private val minimalRequest = VerificationRequest()
|
||||
|
||||
@Test
|
||||
fun `builds params with default config and minimal request`() {
|
||||
val result = QueryParamsBuilder.build(defaultConfig, minimalRequest)
|
||||
assertNotNull(result)
|
||||
assertTrue(result.contains("endpoint=https%3A%2F%2Fapi.self.xyz") || result.contains("endpoint=https://api.self.xyz"))
|
||||
assertTrue(result.contains("environment=prod"))
|
||||
assertTrue(result.contains("version=1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `includes all config params`() {
|
||||
val config =
|
||||
SelfSdkConfig(
|
||||
endpoint = "https://custom.api.xyz",
|
||||
environment = SelfEnvironment.STG,
|
||||
version = 2,
|
||||
appName = "TestApp",
|
||||
appEndpoint = "https://app.endpoint.xyz",
|
||||
endpointType = "custom",
|
||||
chainID = 42,
|
||||
)
|
||||
val result = QueryParamsBuilder.build(config, minimalRequest)
|
||||
assertNotNull(result)
|
||||
assertTrue(result.contains("appName=TestApp"))
|
||||
assertTrue(result.contains("endpointType=custom"))
|
||||
assertTrue(result.contains("chainID=42"))
|
||||
assertTrue(result.contains("environment=stg"))
|
||||
assertTrue(result.contains("version=2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `appEndpoint falls back to endpoint when null`() {
|
||||
val config = SelfSdkConfig(endpoint = "https://api.self.xyz", appEndpoint = null)
|
||||
val result = QueryParamsBuilder.build(config, minimalRequest)
|
||||
assertNotNull(result)
|
||||
val params =
|
||||
result.split("&").associate {
|
||||
val (k, v) = it.split("=", limit = 2)
|
||||
k to v
|
||||
}
|
||||
assertEquals(params["endpoint"], params["appEndpoint"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `includes all request params`() {
|
||||
val request =
|
||||
VerificationRequest(
|
||||
userId = "user-123",
|
||||
scope = "identity",
|
||||
verificationId = "ver-456",
|
||||
resultType = "json",
|
||||
userIdType = "email",
|
||||
userDefinedData = "custom-data",
|
||||
selfDefinedData = "self-data",
|
||||
)
|
||||
val result = QueryParamsBuilder.build(defaultConfig, request)
|
||||
assertNotNull(result)
|
||||
assertTrue(result.contains("userId=user-123"))
|
||||
assertTrue(result.contains("scope=identity"))
|
||||
assertTrue(result.contains("verificationId=ver-456"))
|
||||
assertTrue(result.contains("resultType=json"))
|
||||
assertTrue(result.contains("userIdType=email"))
|
||||
assertTrue(result.contains("userDefinedData=custom-data"))
|
||||
assertTrue(result.contains("selfDefinedData=self-data"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disclosures are comma-joined`() {
|
||||
val request = VerificationRequest(disclosures = listOf("name", "dob", "nationality"))
|
||||
val result = QueryParamsBuilder.build(defaultConfig, request)
|
||||
assertNotNull(result)
|
||||
assertTrue(result.contains("disclosures=name%2Cdob%2Cnationality") || result.contains("disclosures=name,dob,nationality"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `excludedCountries are comma-joined`() {
|
||||
val request = VerificationRequest(excludedCountries = listOf("US", "GB"))
|
||||
val result = QueryParamsBuilder.build(defaultConfig, request)
|
||||
assertNotNull(result)
|
||||
assertTrue(result.contains("excludedCountries=US%2CGB") || result.contains("excludedCountries=US,GB"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty lists are omitted`() {
|
||||
val request = VerificationRequest(disclosures = emptyList(), excludedCountries = emptyList())
|
||||
val result = QueryParamsBuilder.build(defaultConfig, request)
|
||||
assertNotNull(result)
|
||||
assertTrue(!result.contains("disclosures"))
|
||||
assertTrue(!result.contains("excludedCountries"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `null optional fields are omitted`() {
|
||||
val result = QueryParamsBuilder.build(defaultConfig, minimalRequest)
|
||||
assertNotNull(result)
|
||||
assertTrue(!result.contains("userId="))
|
||||
assertTrue(!result.contains("appName="))
|
||||
assertTrue(!result.contains("chainID="))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `urlEncode encodes special characters`() {
|
||||
assertEquals("hello%20world", urlEncode("hello world"))
|
||||
assertEquals("a%26b", urlEncode("a&b"))
|
||||
assertEquals("a%3Db", urlEncode("a=b"))
|
||||
assertEquals("a%2Bb", urlEncode("a+b"))
|
||||
assertEquals("a%25b", urlEncode("a%b"))
|
||||
assertEquals("a%23b", urlEncode("a#b"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `urlEncode preserves safe characters`() {
|
||||
assertEquals("hello-world_123", urlEncode("hello-world_123"))
|
||||
assertEquals("abc/def", urlEncode("abc/def"))
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ actual class SelfSdk private constructor(
|
||||
check(SdkProviderRegistry.isConfigured() && IosProviderRegistry.webView != null) {
|
||||
"SDK providers not configured. " +
|
||||
"Call SelfSdkSwift.configure() from your iOS app before launching the SDK. " +
|
||||
"Required: secureStorage, crypto, and webView providers."
|
||||
"Required: secureStorage and webView providers."
|
||||
}
|
||||
|
||||
// Store callback for later
|
||||
@@ -97,7 +97,7 @@ actual class SelfSdk private constructor(
|
||||
registerHandlers(router!!, lifecycleHandler)
|
||||
|
||||
// Build query params from config + request
|
||||
val queryParams = buildQueryParams(request)
|
||||
val queryParams = QueryParamsBuilder.build(config, request)
|
||||
|
||||
// Create WebView host and the web view
|
||||
webViewHost =
|
||||
@@ -155,44 +155,4 @@ actual class SelfSdk private constructor(
|
||||
router.register(CryptoBridgeHandler())
|
||||
router.register(lifecycleHandler)
|
||||
}
|
||||
|
||||
private fun buildQueryParams(request: VerificationRequest): String? {
|
||||
val parts = mutableListOf<String>()
|
||||
|
||||
// Config params (always present)
|
||||
parts.add("endpoint=${encodeParam(config.endpoint)}")
|
||||
parts.add("appEndpoint=${encodeParam(config.appEndpoint ?: config.endpoint)}")
|
||||
parts.add("environment=${encodeParam(config.environment.queryValue)}")
|
||||
parts.add("version=${config.version}")
|
||||
|
||||
// Optional config params
|
||||
config.appName?.let { parts.add("appName=${encodeParam(it)}") }
|
||||
config.endpointType?.let { parts.add("endpointType=${encodeParam(it)}") }
|
||||
config.chainID?.let { parts.add("chainID=$it") }
|
||||
|
||||
// Request params
|
||||
request.verificationId?.let { parts.add("verificationId=${encodeParam(it)}") }
|
||||
request.userId?.let { parts.add("userId=${encodeParam(it)}") }
|
||||
request.scope?.let { parts.add("scope=${encodeParam(it)}") }
|
||||
if (request.disclosures.isNotEmpty()) {
|
||||
parts.add("disclosures=${encodeParam(request.disclosures.joinToString(","))}")
|
||||
}
|
||||
request.resultType?.let { parts.add("resultType=${encodeParam(it)}") }
|
||||
if (request.excludedCountries.isNotEmpty()) {
|
||||
parts.add("excludedCountries=${encodeParam(request.excludedCountries.joinToString(","))}")
|
||||
}
|
||||
request.userIdType?.let { parts.add("userIdType=${encodeParam(it)}") }
|
||||
request.userDefinedData?.let { parts.add("userDefinedData=${encodeParam(it)}") }
|
||||
request.selfDefinedData?.let { parts.add("selfDefinedData=${encodeParam(it)}") }
|
||||
|
||||
return parts.joinToString("&").ifEmpty { null }
|
||||
}
|
||||
|
||||
private fun encodeParam(value: String): String =
|
||||
value
|
||||
.replace("%", "%25")
|
||||
.replace("&", "%26")
|
||||
.replace("=", "%3D")
|
||||
.replace("+", "%2B")
|
||||
.replace(" ", "%20")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package xyz.self.sdk.webview
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import platform.UIKit.UIView
|
||||
import platform.UIKit.UIViewController
|
||||
import xyz.self.sdk.api.SdkConstants
|
||||
import xyz.self.sdk.bridge.MessageRouter
|
||||
import xyz.self.sdk.providers.IosProviderRegistry
|
||||
|
||||
@@ -14,7 +15,7 @@ import xyz.self.sdk.providers.IosProviderRegistry
|
||||
class IosWebViewHost(
|
||||
private val router: MessageRouter,
|
||||
private val isDebugMode: Boolean = false,
|
||||
private val remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app",
|
||||
private val remoteWebAppBaseUrl: String = SdkConstants.DEFAULT_REMOTE_WEB_APP_BASE_URL,
|
||||
private val devServerUrl: String? = null,
|
||||
) {
|
||||
fun createWebView(queryParams: String? = null): UIView {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Constants shared with the KMP SDK layer.
|
||||
/// Keep in sync with `packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SdkConstants.kt`.
|
||||
enum SdkConstants {
|
||||
static let loopbackHost = "127.0.0.1"
|
||||
static let debugPort = 5173
|
||||
static let diditHost = "verify.didit.me"
|
||||
static let bundledTourPath = "/tunnel/tour/1"
|
||||
static let defaultRemoteWebAppBaseURL = "https://self-app-alpha.vercel.app"
|
||||
}
|
||||
@@ -9,10 +9,10 @@ import WebKit
|
||||
/// Swift implementation of WebViewProvider using WKWebView.
|
||||
/// Handles message passing between the WebView and the KMP bridge.
|
||||
public class WebViewProviderImpl: NSObject {
|
||||
static let loopbackHost = "127.0.0.1"
|
||||
static let diditHost = "verify.didit.me"
|
||||
static let debugPort: UInt16 = 5173
|
||||
private static let defaultRemoteBaseURL = URL(string: "https://self-app-alpha.vercel.app")!
|
||||
static let loopbackHost = SdkConstants.loopbackHost
|
||||
static let diditHost = SdkConstants.diditHost
|
||||
static let debugPort = SdkConstants.debugPort
|
||||
private static let defaultRemoteBaseURL = URL(string: SdkConstants.defaultRemoteWebAppBaseURL)!
|
||||
|
||||
private var webView: WKWebView?
|
||||
private var viewController: UIViewController?
|
||||
@@ -198,7 +198,7 @@ extension WebViewProviderImpl {
|
||||
components.scheme = baseURL.scheme
|
||||
components.host = baseURL.host
|
||||
components.port = baseURL.port
|
||||
components.path = "/tunnel/tour/1"
|
||||
components.path = SdkConstants.bundledTourPath
|
||||
if let queryParams, !queryParams.isEmpty {
|
||||
components.percentEncodedQuery = queryParams
|
||||
}
|
||||
@@ -209,8 +209,8 @@ extension WebViewProviderImpl {
|
||||
var components = URLComponents()
|
||||
components.scheme = "http"
|
||||
components.host = Self.loopbackHost
|
||||
components.port = Int(Self.debugPort)
|
||||
components.path = "/tunnel/tour/1"
|
||||
components.port = Self.debugPort
|
||||
components.path = SdkConstants.bundledTourPath
|
||||
if let queryParams, !queryParams.isEmpty {
|
||||
components.percentEncodedQuery = queryParams
|
||||
}
|
||||
@@ -223,7 +223,7 @@ extension WebViewProviderImpl {
|
||||
components.scheme = remoteWebAppBaseURL.scheme
|
||||
components.host = remoteWebAppBaseURL.host
|
||||
if let port = remoteWebAppBaseURL.port { components.port = port }
|
||||
components.path = "/tunnel/tour/1"
|
||||
components.path = SdkConstants.bundledTourPath
|
||||
if let queryParams, !queryParams.isEmpty {
|
||||
components.percentEncodedQuery = queryParams
|
||||
}
|
||||
@@ -244,7 +244,7 @@ extension WebViewProviderImpl {
|
||||
if let devUrl = devServerUrl, !devUrl.isEmpty, let devBase = URL(string: devUrl) {
|
||||
return url.scheme == devBase.scheme && url.host == devBase.host && resolvedPort(for: url) == resolvedPort(for: devBase)
|
||||
}
|
||||
return url.scheme == "http" && url.host == Self.loopbackHost && url.port == Int(Self.debugPort)
|
||||
return url.scheme == "http" && url.host == Self.loopbackHost && url.port == Self.debugPort
|
||||
}
|
||||
#endif
|
||||
return url.scheme == remoteWebAppBaseURL.scheme &&
|
||||
@@ -259,7 +259,7 @@ extension WebViewProviderImpl {
|
||||
let expectedPort = resolvedPort(for: devBase)
|
||||
return origin.protocol == devBase.scheme && origin.host == devBase.host && resolvedSecurityOriginPort(origin) == expectedPort
|
||||
}
|
||||
return origin.protocol == "http" && origin.host == Self.loopbackHost && origin.port == Int(Self.debugPort)
|
||||
return origin.protocol == "http" && origin.host == Self.loopbackHost && origin.port == Self.debugPort
|
||||
}
|
||||
#endif
|
||||
let expectedPort = resolvedPort(for: remoteWebAppBaseURL)
|
||||
|
||||
@@ -18,7 +18,6 @@ import Foundation
|
||||
/// SdkProviderRegistry.shared.biometric = SelfSdkSwift.biometric
|
||||
/// SdkProviderRegistry.shared.secureStorage = SelfSdkSwift.secureStorage
|
||||
/// SdkProviderRegistry.shared.haptic = SelfSdkSwift.haptic
|
||||
/// SdkProviderRegistry.shared.crypto = SelfSdkSwift.crypto
|
||||
/// SdkProviderRegistry.shared.documents = SelfSdkSwift.documents
|
||||
/// SdkProviderRegistry.shared.webView = SelfSdkSwift.webView
|
||||
/// SdkProviderRegistry.shared.nfc = SelfSdkSwift.nfc
|
||||
@@ -30,7 +29,6 @@ public final class SelfSdkSwift {
|
||||
public static let biometric = BiometricProviderImpl()
|
||||
public static let secureStorage = SecureStorageProviderImpl()
|
||||
public static let haptic = HapticProviderImpl()
|
||||
public static let crypto = CryptoProviderImpl()
|
||||
public static let documents = DocumentsProviderImpl()
|
||||
public static let webView = WebViewProviderImpl()
|
||||
public static let nfc = NfcProviderImpl()
|
||||
|
||||
@@ -89,6 +89,8 @@ function findLicenseHeaderIndex(lines) {
|
||||
let i = 0;
|
||||
// Skip shebang if present
|
||||
if (lines[i]?.startsWith('#!')) i++;
|
||||
// Skip swift-tools-version (must be first line in Package.swift)
|
||||
if (lines[i]?.startsWith('// swift-tools-version')) i++;
|
||||
// Skip leading blank lines
|
||||
while (i < lines.length && lines[i].trim() === '') i++;
|
||||
|
||||
@@ -187,10 +189,24 @@ function fixLicenseHeader(filePath) {
|
||||
|
||||
if (headerInfo.index === -1) {
|
||||
// No header exists - add the canonical header
|
||||
// Preserve shebang and swift-tools-version prefixes
|
||||
let insertIndex = 0;
|
||||
if (lines[insertIndex]?.startsWith('#!')) {
|
||||
insertIndex += 1;
|
||||
}
|
||||
if (lines[insertIndex]?.startsWith('// swift-tools-version')) {
|
||||
insertIndex += 1;
|
||||
// Ensure blank line between tools-version and license header
|
||||
if (lines[insertIndex]?.trim() !== '') {
|
||||
lines.splice(insertIndex, 0, '');
|
||||
}
|
||||
insertIndex += 1;
|
||||
}
|
||||
const newLines = [
|
||||
...lines.slice(0, insertIndex),
|
||||
...CANONICAL_HEADER_LINES,
|
||||
'', // Add newline after header
|
||||
...lines,
|
||||
...lines.slice(insertIndex),
|
||||
];
|
||||
const fixedContent = newLines.join('\n');
|
||||
writeFileSync(filePath, fixedContent, 'utf8');
|
||||
|
||||
Reference in New Issue
Block a user