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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@

## Next

### 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.
- **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

- **Android**: Add `presentCaptiveSigningWithUrl` support. The URL flow now has iOS/Android parity and does not require `loginWithAccessToken`.
- **Android**: Add an opt-in `launchStrategy` on `presentCaptiveSigning`. `signingUrl` mints a recipient view and launches the SDK's URL overload, skipping the envelope download that runs on a size-derived read timeout floored at 15s and can leave the ceremony unopened on large envelopes. Falls back to `fetch` if the mint fails. Defaults to `fetch`, so upgrading changes nothing unless you opt in.

### Fixes

- **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.

## 1.0.5

Expand Down
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ type CaptiveSigningParams = {
recipientUserName: string;
recipientEmail: string;
recipientClientUserId: string;
launchStrategy?: 'fetch' | 'signingUrl'; // Android only, default 'fetch'
};

type SigningResult = {
Expand All @@ -346,11 +347,38 @@ type SigningResult = {
- `envelopeId`: the DocuSign envelope ID created by your backend
- `recipientUserName`, `recipientEmail`: must match the recipient registered on the envelope
- `recipientClientUserId`: the `clientUserId` of the embedded recipient, used by DocuSign to identify captive signers
- `launchStrategy`: how the Android SDK opens the ceremony, see [Android launch strategies](#android-launch-strategies). Ignored on iOS.

**Throws:** rejects with `signing_failed` if the SDK fails to present the signing UI (e.g. not initialized, not logged in, invalid envelope).
**Throws:**

- `not_initialized` if `initialize` has not been called
- `not_logged_in` if `loginWithAccessToken` has not been called
- `signing_failed` if the SDK fails to present the signing UI (e.g. invalid envelope, or a signing session already in progress)

**Returns:** resolves with a `SigningResult` once the user completes or cancels. `status === 'completed'` means the user finished the signing ceremony. `status === 'cancelled'` means the user explicitly cancelled or closed the signing UI.

#### Android launch strategies

The Android SDK can open a captive signing ceremony two ways, and `launchStrategy` picks between them. It has no effect on iOS.

`'fetch'` is the default and matches every release before this option existed. The SDK downloads the envelope with `include=documents` and then opens the ceremony. That download runs on a read timeout derived from the envelope size, which floors at 15 seconds when nothing is cached, so a large envelope on a slow connection can exhaust it and the ceremony never opens.

`'signingUrl'` skips the download. The module mints a recipient view with the session access token (`POST /accounts/{accountId}/envelopes/{envelopeId}/views/recipient`) and points the SDK straight at the returned URL, so the call that times out never runs.

```ts
await presentCaptiveSigning({
envelopeId,
recipientUserName,
recipientEmail,
recipientClientUserId,
launchStrategy: 'signingUrl',
});
```

Before opting in, check that the access token you pass to `loginWithAccessToken` is scoped to create recipient views on the envelope. If the mint fails the module falls back to `'fetch'`, so the worst case is a wasted round trip per ceremony rather than a failure, but there is no point paying for it if the token cannot mint.

If your backend already mints recipient view URLs, prefer `presentCaptiveSigningWithUrl` instead. It keeps the DocuSign access token off the device entirely, which is the better shape. `'signingUrl'` exists for teams who cannot change their backend.

### `presentCaptiveSigningWithUrl(params: CaptiveSigningUrlParams): Promise<SigningResult>`

Presents the DocuSign signing UI using a pre-minted recipient view URL (obtained server-side from `POST /envelopes/{id}/views/recipient`). Bypasses SDK authentication; `initialize` is required, but `loginWithAccessToken` is not.
Expand All @@ -371,8 +399,8 @@ type CaptiveSigningUrlParams = {

**Throws:**

- rejects if `initialize` has not been called
- `signing_failed` if the URL is expired, malformed, or rejected by DocuSign
- `not_initialized` if `initialize` has not been called
- `signing_failed` if the URL is blank or not `https`, or if it is expired or rejected by DocuSign

**Returns:** same `SigningResult` shape as `presentCaptiveSigning`.

Expand Down
39 changes: 29 additions & 10 deletions android/src/main/java/expo/modules/docusign/DocuSignError.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,36 @@ package expo.modules.docusign

import expo.modules.kotlin.exception.CodedException

class NotInitializedException :
CodedException("DocuSign SDK has not been initialized. Call initialize() first.")
// Codes are given explicitly rather than inferred. CodedException derives a code from the class
// name when none is provided, which would surface NotInitializedException to JS as
// ERR_NOT_INITIALIZED, not the not_initialized documented in the README error table.

class NotLoggedInException :
CodedException("DocuSign SDK is not logged in. Call loginWithAccessToken() first.")
class NotInitializedException : CodedException(
"not_initialized",
"DocuSign SDK has not been initialized. Call initialize() first.",
null
)

class LoginFailedException(message: String) :
CodedException("DocuSign login failed: $message")
class NotLoggedInException : CodedException(
"not_logged_in",
"DocuSign SDK is not logged in. Call loginWithAccessToken() first.",
null
)

class SigningFailedException(message: String) :
CodedException("DocuSign signing failed: $message")
class LoginFailedException(message: String) : CodedException(
"login_failed",
"DocuSign login failed: $message",
null
)

class PresentationException(message: String) :
CodedException("Failed to present DocuSign signing UI: $message")
class SigningFailedException(message: String) : CodedException(
"signing_failed",
"DocuSign signing failed: $message",
null
)

class PresentationException(message: String) : CodedException(
"presentation_failed",
"Failed to present DocuSign signing UI: $message",
null
)
Loading
Loading