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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
.vscode/
jsconfig.json

# Coding agents
.claude/

# Xcode
#
*.pbxuser
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
### Breaking changes

- **Android**: rejection codes now reflect the failure. `presentCaptiveSigning` and `presentCaptiveSigningWithUrl` previously rejected every error as `signing_failed`; they now surface `not_initialized`, `not_logged_in`, `login_failed` or `signing_failed`, matching the codes the error table has always documented. Callers matching on `error.code === 'signing_failed'` to detect a missing `initialize()` or `loginWithAccessToken()` need to match the specific code instead.
- **iOS**: rejection codes now match the documented table and the Android module. Expo derives a code from the exception class name when none is set, so `not_initialized` reached JS as `ERR_NOT_INITIALIZED`, `signing_failed` as `ERR_SIGNING_FAILED`, and so on for every code the README has always listed. Callers matching on the `ERR_`-prefixed variants need to match the documented code instead.
- **iOS**: `presentCaptiveSigning` and `presentCaptiveSigningWithUrl` forward the failure's own code rather than rejecting everything as `signing_failed`. A failure to find a presenting view controller now rejects and emits `presentation_failed`. The rejection message is the underlying error text on its own, where it previously carried a `DocuSign signing failed:` prefix.
- **Android**: one `onSigningError` event per failure instead of two. The module emitted an event alongside the manager's own, which also flattened `recipient_signing_failed` into `signing_failed`. Listeners that deduplicated by hand can drop that workaround; listeners that counted events will see the count halve.

### New features
Expand All @@ -14,6 +16,7 @@

### Fixes

- **iOS**: reject a blank or non-`https` `signingUrl` before presenting. `DSMEnvelopesManager.presentCaptiveSigning` validates nothing and presents unconditionally, so a malformed URL rendered an empty signing controller whose completion never fired and left the promise unsettled. `signingUrl` defaults to `""` when JS omits it, so this was reachable without a malformed URL at all. Brings iOS to parity with the Android guard below.
- **Android**: reject a blank or non-`https` `signingUrl` before launching. The SDK's URL overload validates nothing and calls `startActivity` unconditionally, so a malformed URL opened an empty signing activity and left the promise unsettled.
- **Android**: `presentCaptiveSigning` now clears `currentEnvelopeId` when the launch itself throws, matching the URL path.

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,9 @@ The module rejects promises with coded exceptions you can inspect at the call si
| `signing_failed` | SDK failed to present or complete signing | Check envelope ID, recipient info, SDK login state |
| `not_initialized` | `initialize()` was not called first | Call `initialize()` before any other method |
| `not_logged_in` | `loginWithAccessToken()` was not called first | Call `loginWithAccessToken()` before `presentCaptiveSigning()` |
| `presentation_failed` | No view controller available to present from (iOS) | Present from a mounted screen, not during a navigation transition |

Both platforms emit these codes verbatim. Do not match on `ERR_`-prefixed variants.

Example:

Expand Down
24 changes: 24 additions & 0 deletions ios/DocuSignError.swift
Original file line number Diff line number Diff line change
@@ -1,30 +1,54 @@
import ExpoModulesCore

// Codes are given explicitly rather than inferred. Expo derives a code from the class name when
// none is set, which would surface NotInitializedException to JS as ERR_NOT_INITIALIZED, not the
// not_initialized documented in the README error table and emitted by the Android module.

internal class NotInitializedException: Exception {
override var code: String {
"not_initialized"
}

override var reason: String {
"DocuSign SDK has not been initialized. Call initialize() first."
}
}

internal class NotLoggedInException: Exception {
override var code: String {
"not_logged_in"
}

override var reason: String {
"DocuSign SDK is not logged in. Call loginWithAccessToken() first."
}
}

internal class PresentationException: GenericException<String> {
override var code: String {
"presentation_failed"
}

override var reason: String {
"Failed to present DocuSign signing UI: \(param)"
}
}

internal class SigningFailedException: GenericException<String> {
override var code: String {
"signing_failed"
}

override var reason: String {
"DocuSign signing failed: \(param)"
}
}

internal class LoginFailedException: GenericException<String> {
override var code: String {
"login_failed"
}

override var reason: String {
"DocuSign login failed: \(param)"
}
Expand Down
19 changes: 19 additions & 0 deletions ios/DocuSignManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,19 @@ internal final class DocuSignManager: NSObject {
}
}

/// Guards the URL handed to the SDK's URL overload.
///
/// `DSMEnvelopesManager.presentCaptiveSigning(withPresenting:signingUrl:...)` validates nothing
/// and presents unconditionally, so a blank or non-https URL renders an empty signing controller
/// whose completion never fires and leaves the promise unsettled. `CaptiveSigningUrlRecord`
/// defaults `signingUrl` to "", so an omitted field reaches here as a blank string.
private static func isHttpsUrl(_ url: String) -> Bool {
guard let components = URLComponents(string: url) else {
return false
}
return components.scheme?.lowercased() == "https" && !(components.host ?? "").isEmpty
}

/// Presents captive signing from a pre-minted DocuSign recipient-view URL.
///
/// The signing URL itself encodes recipient identity via a short-lived token,
Expand All @@ -687,6 +700,12 @@ internal final class DocuSignManager: NSObject {
throw NotInitializedException()
}

// Ahead of the pendingCompletion claim on purpose: a rejected URL must not occupy the slot, or
// a later valid call would be refused as "already in progress".
guard Self.isHttpsUrl(signingUrl) else {
throw SigningFailedException("Signing URL must be a valid HTTPS URL")
}

var alreadyInFlight = false
stateQueue.sync {
if pendingCompletion != nil {
Expand Down
18 changes: 14 additions & 4 deletions ios/DocuSignModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,17 @@ public class DocuSignModule: Module {
"errorMessage": outcome.errorMessage as Any
])
case .failure(let error):
// Forward the failure's own code. Hard-coding signing_failed flattened
// presentation_failed and hid which stage failed. promise.reject(error) is not the
// alternative here: it wraps anything that is not an Exception, so a raw SDK NSError
// would surface as ERR_UNEXPECTED.
let code = (error as? CodedError)?.code ?? "signing_failed"
self.sendEvent("onSigningError", [
"envelopeId": params.envelopeId,
"errorCode": "signing_failed",
"errorCode": code,
"errorMessage": error.localizedDescription
])
promise.reject(SigningFailedException(error.localizedDescription))
promise.reject(code, error.localizedDescription)
}
}
} catch {
Expand All @@ -138,12 +143,17 @@ public class DocuSignModule: Module {
"errorMessage": outcome.errorMessage as Any
])
case .failure(let error):
// Forward the failure's own code. Hard-coding signing_failed flattened
// presentation_failed and hid which stage failed. promise.reject(error) is not the
// alternative here: it wraps anything that is not an Exception, so a raw SDK NSError
// would surface as ERR_UNEXPECTED.
let code = (error as? CodedError)?.code ?? "signing_failed"
self.sendEvent("onSigningError", [
"envelopeId": params.envelopeId,
"errorCode": "signing_failed",
"errorCode": code,
"errorMessage": error.localizedDescription
])
promise.reject(SigningFailedException(error.localizedDescription))
promise.reject(code, error.localizedDescription)
}
}
} catch {
Expand Down
Loading