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
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,5 @@ android {
dependencies {
implementation("circle.programmablewallet:sdk:1.0.1102")
implementation("com.github.bumptech.glide:glide:4.16.0")
testImplementation("junit:junit:4.13.2")
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package com.circlefin.programmablewalletrnsdk
import android.util.Log
import circle.programmablewallet.sdk.WalletSdk
import circle.programmablewallet.sdk.WalletSdk.init
import circle.programmablewallet.sdk.api.ApiError
import circle.programmablewallet.sdk.api.ExecuteEvent
import circle.programmablewallet.sdk.api.SocialProvider
import circle.programmablewallet.sdk.api.SocialCallback
Expand Down Expand Up @@ -88,6 +89,20 @@ class ProgrammablewalletRnSdkModule : Module(), EventListener {
WalletSdk.Configuration(endpoint, appId, settings)
)
promise.resolve(emptyMap<String, Any>())
} catch (e: ApiError) {
// Surface the real ApiError code + message to JS. The converted
// message (convertApiErrorToMap) applies setErrorStringMap overrides
// — unlike PromiseCallback's dismiss branch, which uses the raw
// error.message; keep that distinction if consolidating paths.
// resolveInitErrorMessage falls back to the raw error.message when
// the converted message is blank (e.g. a blank override) or absent,
// then to a generic default, so the JS error contract (message
// always present and UI-readable) always holds.
val message = RecordsHelper.resolveInitErrorMessage(
RecordsHelper.convertApiErrorToMap(e)["message"] as? String,
e.message
)
promise.reject(CodedException(e.code.value.toString(), message, e))
} catch (e: Throwable) {
promise.reject(CodedException(e))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,22 @@ object RecordsHelper {
)
}

/**
* Resolve the JS-facing error message for initSdk failures.
*
* [convertedMessage] is `convertApiErrorToMap(error)["message"]`, which is
* `customMessage ?: error.message` with no blank check — so a **blank**
* `setErrorStringMap` override is returned as-is (not treated as absent).
* The [rawMessage] fallback (the SDK's `error.message`) is therefore NOT
* dead code: it recovers the raw message when the override is blank as well
* as when the converted message is absent. Falls back to a generic default
* so the JS error contract (message always present and UI-readable) holds.
*/
fun resolveInitErrorMessage(convertedMessage: String?, rawMessage: String?): String =
convertedMessage?.takeIf { it.isNotBlank() }
?: rawMessage?.takeIf { it.isNotBlank() }
?: "SDK initialization failed"

fun convertExecuteWarningToMap(warning: ExecuteWarning): Map<String, Any?> {
return mapOf(
"warningType" to warning.warningType,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2025 Circle Internet Group, Inc. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.circlefin.programmablewalletrnsdk

import org.junit.Assert.assertEquals
import org.junit.Test

/**
* Unit tests for [RecordsHelper.resolveInitErrorMessage], the pure fallback
* used by initSdk's ApiError catch to build the JS-facing error message.
*/
class RecordsHelperTest {

@Test
fun usesConvertedMessageWhenNonBlank() {
assertEquals(
"Invalid appId: bad",
RecordsHelper.resolveInitErrorMessage("Invalid appId: bad", "raw sdk message"),
)
}

@Test
fun fallsBackToRawMessageWhenConvertedIsBlankOverride() {
// A blank setErrorStringMap override is returned as-is by
// convertApiErrorToMap (customMessage ?: error.message, no blank check),
// so the raw error.message fallback must recover the useful message.
assertEquals(
"raw sdk message",
RecordsHelper.resolveInitErrorMessage(" ", "raw sdk message"),
)
}

@Test
fun fallsBackToRawMessageWhenConvertedIsAbsent() {
assertEquals(
"raw sdk message",
RecordsHelper.resolveInitErrorMessage(null, "raw sdk message"),
)
}

@Test
fun fallsBackToGenericDefaultWhenBothBlankOrNull() {
assertEquals(
"SDK initialization failed",
RecordsHelper.resolveInitErrorMessage("", " "),
)
assertEquals(
"SDK initialization failed",
RecordsHelper.resolveInitErrorMessage(null, null),
)
}
}
50 changes: 37 additions & 13 deletions ios/ProgrammablewalletRnSdkModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,29 +59,43 @@ public class ProgrammablewalletRnSdkModule: Module {
}

// Initialize SDK with configuration
AsyncFunction("initSdk") { (configuration: [String: Any]) async throws -> Void in

WalletSdk.shared.setLayoutProvider(self)
WalletSdk.shared.setDelegate(self)
WalletSdk.shared.setErrorMessenger(self)

// Unlike execute/performLogin/performLogout, this does not dispatch to
// the main queue: setConfiguration only validates input and assigns
// properties (no UIKit work), so it is safe to run inline.
AsyncFunction("initSdk") { (configuration: [String: Any], promise: Promise) in

var endPoint = configuration["endpoint"] as? String ?? ""
if endPoint.last == "/" {
endPoint.removeLast()
}
let appId = configuration["appId"] as? String ?? ""

var enableBiometricsPin = false
if let settingsManagement = configuration["settingsManagement"] as? [String: Any],
let enableBio = settingsManagement["enableBiometricsPin"] as? Bool {
enableBiometricsPin = enableBio
}

let settings = WalletSdk.SettingsManagement(enableBiometricsPin: enableBiometricsPin)
let sdkConfig = WalletSdk.Configuration(endPoint: endPoint, appId: appId, settingsManagement: settings)

try WalletSdk.shared.setConfiguration(sdkConfig)
return

do {
try WalletSdk.shared.setConfiguration(sdkConfig)
// Only wire the shared singleton to this module after config
// validation succeeds, so a failed configure doesn't leave the
// singleton wired ahead of a valid configuration.
WalletSdk.shared.setLayoutProvider(self)
WalletSdk.shared.setDelegate(self)
WalletSdk.shared.setErrorMessenger(self)
promise.resolve(nil)
} catch let error as ApiError {
// Surface the real ApiError code + errorString to JS, consistent with
// the other Promise-based operations (performLogin / performLogout).
promise.reject(String(error.errorCode.rawValue), self._bridgePromiseErrorMessage(error))
} catch {
let nsError = error as NSError
promise.reject(String(nsError.code), nsError.localizedDescription)
}
}

// Set security questions for the wallet
Expand Down Expand Up @@ -156,8 +170,18 @@ public class ProgrammablewalletRnSdkModule: Module {
return
}
DispatchQueue.main.async {
WalletSdk.shared.performLogout(provider: socialProvider) { _ in
promise.resolve(nil)
WalletSdk.shared.performLogout(provider: socialProvider) { logoutResult in
switch logoutResult {
case .success:
promise.resolve(nil)
case .failure(let error):
if let apiError = error as? ApiError {
promise.reject(String(apiError.errorCode.rawValue), self._bridgePromiseErrorMessage(apiError))
} else {
let nsError = error as NSError
promise.reject(String(nsError.code), nsError.localizedDescription)
}
}
}
}
}
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@circle-fin/w3s-pw-react-native-sdk",
"version": "2.2.2",
"version": "2.2.3",
"packageManager": "yarn@1.22.22",
"description": "React Native SDK for Circle Programmable Wallet",
"main": "build/index.js",
Expand Down Expand Up @@ -106,8 +106,11 @@
},
"resolutions": {
"@xmldom/xmldom": "0.8.13",
"form-data": "4.0.6",
"js-yaml": "4.3.0",
"node-forge": "1.4.0",
"postcss": "8.5.10",
"undici": "6.25.0"
"postcss": "8.5.22",
"undici": "6.27.0",
"uuid": "11.1.1"
}
}
19 changes: 19 additions & 0 deletions plugins/__test__/podfile-modifier.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const withPodfileModifier = require('../podfile-modifier')

describe('withPodfileModifier', () => {
it('is exported as a function', () => {
expect(typeof withPodfileModifier).toBe('function')
})

// Spec (docs/specs/rn-sdk-expo-plugins.md): "podfile-modifier and
// apple-signin-entitlements take no options." This guards against
// reintroducing an `options` parameter (CCS-4809). Function#length is
// unreliable here — a defaulted parameter (`options = {}`) does not count
// toward it — so assert on the declared signature instead.
it('declares only the config parameter — no options (spec: no-options contract)', () => {
const source = withPodfileModifier.toString()
const signature = source.slice(0, source.indexOf('=>'))
expect(signature).not.toMatch(/options/)
expect(signature.replace(/[()\s]/g, '')).toBe('config')
})
})
2 changes: 1 addition & 1 deletion plugins/podfile-modifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const path = require('path')
/**
* Simple Podfile modifier plugin for Circle SDK
*/
const withPodfileModifier = (config, options = {}) => {
const withPodfileModifier = config => {
return withDangerousMod(config, [
'ios',
async config => {
Expand Down
Loading
Loading