From 0a98b08da1be05162a148b8436e4efe4a101dd3a Mon Sep 17 00:00:00 2001 From: Amit Sharma Date: Wed, 19 Aug 2026 18:02:04 +0530 Subject: [PATCH 1/3] feat(turbosign): support conditional (IF/THEN) fields across all SDKs A signature field can now carry an optional `metadata` object that makes it depend on a controlling checkbox: set `metadata.fieldKey` on a `checkbox` field, and `metadata.conditional` = { controllingFieldKey, operator, action } on a dependent field. `operator` is "is_checked" | "is_not_checked"; `action` is "show" (hidden until met) | "unlock" (visible but read-only until met). It flows through the single-step prepare-for-signing / prepare-for-review routes and rides along in the already-serialized fields payload. Implemented consistently in all six SDKs (JS/TS, Python, Go, PHP, Java, Ruby) with matching types, examples, README sections, and tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 11 ++- packages/go-sdk/README.md | 40 +++++++++++ .../go-sdk/examples/turbosign_advanced.go | 30 ++++++++ packages/go-sdk/turbosign.go | 49 +++++++++++++ packages/go-sdk/turbosign_test.go | 68 +++++++++++++++++++ packages/java-sdk/README.md | 42 ++++++++++++ .../java-sdk/examples/TurboSignAdvanced.java | 27 ++++++++ .../main/java/com/turbodocx/ManualTest.java | 8 ++- .../main/java/com/turbodocx/models/Field.java | 17 ++++- .../turbodocx/models/FieldConditional.java | 39 +++++++++++ .../com/turbodocx/models/FieldMetadata.java | 40 +++++++++++ .../java/com/turbodocx/TurboSignTest.java | 52 ++++++++++++++ .../turbodocx/models/FieldBuilderTest.java | 50 ++++++++++++++ packages/js-sdk/README.md | 41 +++++++++++ .../js-sdk/examples/turbosign-advanced.ts | 32 +++++++++ packages/js-sdk/src/types/sign.ts | 49 +++++++++++++ packages/js-sdk/tests/turbosign.test.ts | 65 ++++++++++++++++++ packages/php-sdk/README.md | 50 +++++++++++++- .../php-sdk/examples/turbosign-advanced.php | 36 ++++++++++ .../php-sdk/src/Types/ConditionalAction.php | 16 +++++ .../php-sdk/src/Types/ConditionalOperator.php | 16 +++++ packages/php-sdk/src/Types/Field.php | 7 ++ .../php-sdk/src/Types/FieldConditional.php | 40 +++++++++++ packages/php-sdk/src/Types/FieldMetadata.php | 43 ++++++++++++ packages/php-sdk/tests/Unit/FieldTest.php | 48 +++++++++++++ packages/py-sdk/README.md | 42 ++++++++++++ .../py-sdk/examples/turbosign_advanced.py | 30 ++++++++ .../py-sdk/src/turbodocx_sdk/modules/sign.py | 14 ++++ packages/py-sdk/tests/test_turbosign.py | 54 +++++++++++++++ packages/ruby-sdk/README.md | 42 ++++++++++++ .../ruby-sdk/examples/turbosign_advanced.rb | 30 ++++++++ packages/ruby-sdk/spec/turbo_sign_spec.rb | 49 +++++++++++++ 32 files changed, 1169 insertions(+), 8 deletions(-) create mode 100644 packages/java-sdk/src/main/java/com/turbodocx/models/FieldConditional.java create mode 100644 packages/java-sdk/src/main/java/com/turbodocx/models/FieldMetadata.java create mode 100644 packages/php-sdk/src/Types/ConditionalAction.php create mode 100644 packages/php-sdk/src/Types/ConditionalOperator.php create mode 100644 packages/php-sdk/src/Types/FieldConditional.php create mode 100644 packages/php-sdk/src/Types/FieldMetadata.php diff --git a/README.md b/README.md index 90cbc966..62cc4bdf 100644 --- a/README.md +++ b/README.md @@ -345,7 +345,7 @@ await TurboSign.resend(documentId, ['recipient-uuid']); | `initials` | Initials field | | `text` | Free-form text input | | `date` | Date stamp | -| `checkbox` | Checkbox / agreement | +| `checkbox` | Checkbox / agreement — also the **controlling** field for conditional (IF/THEN) logic | | `full_name` | Full name | | `first_name` | First name | | `last_name` | Last name | @@ -353,6 +353,15 @@ await TurboSign.resend(documentId, ['recipient-uuid']); | `title` | Job title | | `company` | Company name | +#### Conditional (IF/THEN) fields + +Any field accepts an optional `metadata` object for conditional logic. Set `metadata.fieldKey` on a +controlling `checkbox`, then set `metadata.conditional` (`controllingFieldKey`, `operator`: +`is_checked` | `is_not_checked`, `action`: `show` | `unlock`) on a dependent field that references +that key. `show` hides the dependent field until the condition is met; `unlock` keeps it visible +but read-only until met. See each SDK's README "Conditional (IF/THEN) Fields" section for the +language-specific form. + --- ## Requirements diff --git a/packages/go-sdk/README.md b/packages/go-sdk/README.md index 3e59ff90..b44093e6 100644 --- a/packages/go-sdk/README.md +++ b/packages/go-sdk/README.md @@ -949,6 +949,45 @@ if err == nil { | `title` | Job title | | `company` | Company name | +The `checkbox` type doubles as the **controlling** field for conditional logic (see below). + +### Conditional (IF/THEN) Fields + +Any field may carry an optional `Metadata` (`*FieldMetadata`) that drives conditional logic. Set +`FieldKey` on a **controlling** `checkbox` to give it a stable id, then set `Conditional` on a +**dependent** field that references that id: + +```go +Fields: []turbodocx.Field{ + // Controlling checkbox — carries the FieldKey dependents reference + { + Type: "checkbox", + RecipientEmail: "john@example.com", + Metadata: &turbodocx.FieldMetadata{FieldKey: "request_changes"}, + }, + // Dependent text field — hidden until the checkbox is checked ("If checked, explain") + { + Type: "text", + RecipientEmail: "john@example.com", + IsMultiline: true, + Metadata: &turbodocx.FieldMetadata{ + Conditional: &turbodocx.FieldConditional{ + ControllingFieldKey: "request_changes", // must equal the checkbox's FieldKey + Operator: turbodocx.ConditionalOperatorIsChecked, // IsChecked | IsNotChecked + Action: turbodocx.ConditionalActionShow, // Show (hidden until met) | Unlock (read-only until met) + }, + }, + }, +} +``` + +| `Metadata` field | Set on | Meaning | +|:-----------------|:-------|:--------| +| `FieldKey` | controlling `checkbox` | Stable client id (≤100 chars) that dependents reference | +| `Conditional.ControllingFieldKey` | dependent field | Must equal the controlling checkbox's `FieldKey` | +| `Conditional.Operator` | dependent field | `ConditionalOperatorIsChecked` (`"is_checked"`) or `ConditionalOperatorIsNotChecked` (`"is_not_checked"`) | +| `Conditional.Action` | dependent field | `ConditionalActionShow` (hidden until met) or `ConditionalActionUnlock` (visible but read-only until met) | + --- ## Type Reference @@ -982,6 +1021,7 @@ type Field struct { Required bool `json:"required,omitempty"` // Field is required BackgroundColor string `json:"backgroundColor,omitempty"` // Background color (hex) Template *TemplateAnchor `json:"template,omitempty"` // Template anchor for dynamic positioning + Metadata *FieldMetadata `json:"metadata,omitempty"` // Conditional (IF/THEN) logic — see "Conditional Fields" } ``` diff --git a/packages/go-sdk/examples/turbosign_advanced.go b/packages/go-sdk/examples/turbosign_advanced.go index 0be9ad15..6b816d81 100644 --- a/packages/go-sdk/examples/turbosign_advanced.go +++ b/packages/go-sdk/examples/turbosign_advanced.go @@ -128,6 +128,36 @@ func main() { Size: &turbodocx.Size{Width: 200, Height: 50}, }, }, + // Conditional (IF/THEN) fields + // Controlling checkbox: carries a stable FieldKey that dependents reference. + { + Type: "checkbox", + RecipientEmail: "john@example.com", + Template: &turbodocx.TemplateAnchor{ + Anchor: "{request_changes}", + Placement: "replace", + Size: &turbodocx.Size{Width: 20, Height: 20}, + }, + Metadata: &turbodocx.FieldMetadata{FieldKey: "request_changes"}, + }, + // Dependent text field: hidden until the checkbox above is checked ("If checked, explain"). + { + Type: "text", + RecipientEmail: "john@example.com", + IsMultiline: true, + Template: &turbodocx.TemplateAnchor{ + Anchor: "{change_details}", + Placement: "replace", + Size: &turbodocx.Size{Width: 200, Height: 50}, + }, + Metadata: &turbodocx.FieldMetadata{ + Conditional: &turbodocx.FieldConditional{ + ControllingFieldKey: "request_changes", + Operator: turbodocx.ConditionalOperatorIsChecked, + Action: turbodocx.ConditionalActionShow, + }, + }, + }, }, }) diff --git a/packages/go-sdk/turbosign.go b/packages/go-sdk/turbosign.go index 659c6599..ccce5663 100644 --- a/packages/go-sdk/turbosign.go +++ b/packages/go-sdk/turbosign.go @@ -68,6 +68,55 @@ type Field struct { Required bool `json:"required,omitempty"` BackgroundColor string `json:"backgroundColor,omitempty"` Template *TemplateAnchor `json:"template,omitempty"` + // Metadata carries optional conditional (IF/THEN) logic — a FieldKey on a controlling + // checkbox, or a Conditional rule on a dependent field. Omitted when nil. + Metadata *FieldMetadata `json:"metadata,omitempty"` +} + +// ConditionalOperator is how a dependent field's controlling checkbox is tested. +type ConditionalOperator string + +const ( + // ConditionalOperatorIsChecked fires while the controlling checkbox is checked. + ConditionalOperatorIsChecked ConditionalOperator = "is_checked" + // ConditionalOperatorIsNotChecked fires while the controlling checkbox is unchecked. + ConditionalOperatorIsNotChecked ConditionalOperator = "is_not_checked" +) + +// ConditionalAction is what happens to a dependent field until its condition is met. +type ConditionalAction string + +const ( + // ConditionalActionShow keeps the field hidden until the condition is met. + ConditionalActionShow ConditionalAction = "show" + // ConditionalActionUnlock keeps the field visible but read-only until the condition is met. + ConditionalActionUnlock ConditionalAction = "unlock" +) + +// FieldConditional is a conditional (IF/THEN) rule set on a DEPENDENT field. +// +// The dependent field reacts to a CONTROLLING checkbox elsewhere in the same Fields slice. +// The controlling field must be Type "checkbox" and carry Metadata.FieldKey; this rule +// references it by that exact key. +type FieldConditional struct { + // ControllingFieldKey must equal the controlling checkbox's Metadata.FieldKey. + ControllingFieldKey string `json:"controllingFieldKey"` + // Operator is whether the rule fires when the checkbox is checked or unchecked. + Operator ConditionalOperator `json:"operator"` + // Action is whether the dependent field is hidden ("show") or read-only ("unlock") until met. + Action ConditionalAction `json:"action"` +} + +// FieldMetadata is optional per-field metadata for conditional (IF/THEN) logic. +// +// Set FieldKey on a CONTROLLING checkbox to give it a stable client id; set Conditional on a +// DEPENDENT field to make it react to that checkbox. Both sides are authored by the caller in +// the same payload. +type FieldMetadata struct { + // FieldKey is a stable client id (<=100 chars) for a controlling checkbox, referenced by dependents. + FieldKey string `json:"fieldKey,omitempty"` + // Conditional is the rule set on a dependent field. + Conditional *FieldConditional `json:"conditional,omitempty"` } // CreateSignatureReviewLinkRequest is the request for CreateSignatureReviewLink diff --git a/packages/go-sdk/turbosign_test.go b/packages/go-sdk/turbosign_test.go index d70360aa..9b5695a6 100644 --- a/packages/go-sdk/turbosign_test.go +++ b/packages/go-sdk/turbosign_test.go @@ -278,6 +278,74 @@ func TestTurboSignClient_SendSignature(t *testing.T) { }) } +func TestTurboSignClient_SendSignatureConditionalMetadata(t *testing.T) { + // The whole Fields slice is JSON-marshaled into the "fields" form value, so conditional + // (IF/THEN) metadata must ride along untouched. + var capturedFields string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body map[string]string + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + capturedFields = body["fields"] + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "documentId": "doc-conditional", + "status": "UNDER_REVIEW", + "message": "Document sent for signing", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + result, err := client.TurboSign.SendSignature(context.Background(), &SendSignatureRequest{ + FileLink: "https://example.com/doc.pdf", + Recipients: []Recipient{ + {Name: "John Doe", Email: "john@example.com", SigningOrder: 1}, + }, + Fields: []Field{ + // Controlling checkbox + { + Type: "checkbox", + RecipientEmail: "john@example.com", + Metadata: &FieldMetadata{FieldKey: "request_changes"}, + }, + // Dependent field + { + Type: "text", + RecipientEmail: "john@example.com", + Metadata: &FieldMetadata{ + Conditional: &FieldConditional{ + ControllingFieldKey: "request_changes", + Operator: ConditionalOperatorIsChecked, + Action: ConditionalActionShow, + }, + }, + }, + }, + }) + + require.NoError(t, err) + assert.Equal(t, "doc-conditional", result.DocumentID) + + var sentFields []Field + require.NoError(t, json.Unmarshal([]byte(capturedFields), &sentFields)) + require.Len(t, sentFields, 2) + require.NotNil(t, sentFields[0].Metadata) + assert.Equal(t, "request_changes", sentFields[0].Metadata.FieldKey) + require.NotNil(t, sentFields[1].Metadata) + require.NotNil(t, sentFields[1].Metadata.Conditional) + assert.Equal(t, "request_changes", sentFields[1].Metadata.Conditional.ControllingFieldKey) + assert.Equal(t, ConditionalOperatorIsChecked, sentFields[1].Metadata.Conditional.Operator) + assert.Equal(t, ConditionalActionShow, sentFields[1].Metadata.Conditional.Action) +} + func TestTurboSignClient_GetStatus(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/turbosign/documents/doc-123/status", r.URL.Path) diff --git a/packages/java-sdk/README.md b/packages/java-sdk/README.md index d999df1b..89f3f8b6 100644 --- a/packages/java-sdk/README.md +++ b/packages/java-sdk/README.md @@ -947,6 +947,48 @@ if (quote.getPreparedBy() != null) { | `title` | Job title | | `company` | Company name | +The `checkbox` type doubles as the **controlling** field for conditional logic (see below). + +### Conditional (IF/THEN) Fields + +Any field may carry an optional `FieldMetadata` that drives conditional logic. Set a `fieldKey` on +a **controlling** `checkbox` to give it a stable id, then set a `FieldConditional` on a +**dependent** field that references that id: + +```java +import com.turbodocx.models.Field; +import com.turbodocx.models.FieldMetadata; +import com.turbodocx.models.FieldConditional; + +List fields = Arrays.asList( + // Controlling checkbox — carries the fieldKey dependents reference + new Field.Builder() + .type("checkbox") + .recipientEmail("john@example.com") + .template(new Field.TemplateAnchor.Builder().anchor("{request_changes}").placement("replace").size(new Field.Size(20, 20)).build()) + .metadata(FieldMetadata.forFieldKey("request_changes")) + .build(), + // Dependent text field — hidden until the checkbox is checked ("If checked, explain") + new Field.Builder() + .type("text") + .recipientEmail("john@example.com") + .isMultiline(true) + .template(new Field.TemplateAnchor.Builder().anchor("{change_details}").placement("replace").size(new Field.Size(200, 50)).build()) + .metadata(FieldMetadata.forConditional( + new FieldConditional("request_changes", "is_checked", "show"))) + .build() +); +``` + +`FieldConditional(controllingFieldKey, operator, action)`: + +| Argument | Set on | Meaning | +|:---------|:-------|:--------| +| `fieldKey` (on `FieldMetadata`) | controlling `checkbox` | Stable client id (≤100 chars) that dependents reference | +| `controllingFieldKey` | dependent field | Must equal the controlling checkbox's `fieldKey` | +| `operator` | dependent field | `"is_checked"` or `"is_not_checked"` | +| `action` | dependent field | `"show"` (hidden until met) or `"unlock"` (visible but read-only until met) | + --- ## Examples diff --git a/packages/java-sdk/examples/TurboSignAdvanced.java b/packages/java-sdk/examples/TurboSignAdvanced.java index d773c38d..74be1be5 100644 --- a/packages/java-sdk/examples/TurboSignAdvanced.java +++ b/packages/java-sdk/examples/TurboSignAdvanced.java @@ -124,6 +124,33 @@ public static void main(String[] args) { .placement("replace") .size(new Field.Size(200, 50)) .build()) + .build(), + + // Conditional (IF/THEN) fields + // Controlling checkbox: carries a stable fieldKey that dependents reference + new Field.Builder() + .type("checkbox") + .recipientEmail("john@example.com") + .template(new Field.TemplateAnchor.Builder() + .anchor("{request_changes}") + .placement("replace") + .size(new Field.Size(20, 20)) + .build()) + .metadata(FieldMetadata.forFieldKey("request_changes")) + .build(), + + // Dependent text field: hidden until the checkbox above is checked ("If checked, explain") + new Field.Builder() + .type("text") + .recipientEmail("john@example.com") + .isMultiline(true) + .template(new Field.TemplateAnchor.Builder() + .anchor("{change_details}") + .placement("replace") + .size(new Field.Size(200, 50)) + .build()) + .metadata(FieldMetadata.forConditional( + new FieldConditional("request_changes", "is_checked", "show"))) .build() )) .build() diff --git a/packages/java-sdk/src/main/java/com/turbodocx/ManualTest.java b/packages/java-sdk/src/main/java/com/turbodocx/ManualTest.java index 39511971..1a85f4ef 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/ManualTest.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/ManualTest.java @@ -120,7 +120,7 @@ private static String testPrepareForReview() throws IOException { )) .fields(Arrays.asList( new Field("signature", 1, 100, 550, 200, 50, TEST_EMAIL), - new Field("checkbox", 1, 320, 550, 50, 50, TEST_EMAIL, "true", null, null, null, null, null) + new Field("checkbox", 1, 320, 550, 50, 50, TEST_EMAIL, "true", null, null, null, null, null, null) )) .documentName("Review Test Document (fileLink)") .build(); @@ -160,7 +160,8 @@ private static String testPrepareForSigningSingle() throws IOException { null, // isReadonly true, // required null, // backgroundColor - templateAnchor // template anchor config + templateAnchor, // template anchor config + null // metadata (no conditional logic) ); // Coordinate-based field (traditional approach) @@ -177,7 +178,8 @@ private static String testPrepareForSigningSingle() throws IOException { null, // isReadonly null, // required null, // backgroundColor - null // no template (coordinate-based) + null, // no template (coordinate-based) + null // metadata (no conditional logic) ); SendSignatureRequest request = new SendSignatureRequest.Builder() diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/Field.java b/packages/java-sdk/src/main/java/com/turbodocx/models/Field.java index 6b2587d5..1df8bb6e 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/models/Field.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/Field.java @@ -47,15 +47,18 @@ public class Field { @SerializedName("template") private final TemplateAnchor template; + @SerializedName("metadata") + private final FieldMetadata metadata; + // Simple constructor for coordinate-based fields public Field(String type, int page, int x, int y, int width, int height, String recipientEmail) { - this(type, page, x, y, width, height, recipientEmail, null, null, null, null, null, null); + this(type, page, x, y, width, height, recipientEmail, null, null, null, null, null, null, null); } // Full constructor public Field(String type, Integer page, Integer x, Integer y, Integer width, Integer height, String recipientEmail, String defaultValue, Boolean isMultiline, Boolean isReadonly, - Boolean required, String backgroundColor, TemplateAnchor template) { + Boolean required, String backgroundColor, TemplateAnchor template, FieldMetadata metadata) { this.type = type; this.page = page; this.x = x; @@ -69,6 +72,7 @@ public Field(String type, Integer page, Integer x, Integer y, Integer width, Int this.required = required; this.backgroundColor = backgroundColor; this.template = template; + this.metadata = metadata; } public String getType() { return type; } @@ -84,6 +88,7 @@ public Field(String type, Integer page, Integer x, Integer y, Integer width, Int public Boolean getRequired() { return required; } public String getBackgroundColor() { return backgroundColor; } public TemplateAnchor getTemplate() { return template; } + public FieldMetadata getMetadata() { return metadata; } /** * Template anchor configuration for dynamic field positioning @@ -246,6 +251,7 @@ public static class Builder { private Boolean required; private String backgroundColor; private TemplateAnchor template; + private FieldMetadata metadata; public Builder type(String type) { this.type = type; @@ -312,6 +318,11 @@ public Builder template(TemplateAnchor template) { return this; } + public Builder metadata(FieldMetadata metadata) { + this.metadata = metadata; + return this; + } + public Field build() { // Validation if (type == null || type.trim().isEmpty()) { @@ -323,7 +334,7 @@ public Field build() { return new Field(type, page, x, y, width, height, recipientEmail, defaultValue, isMultiline, isReadonly, required, - backgroundColor, template); + backgroundColor, template, metadata); } } } diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/FieldConditional.java b/packages/java-sdk/src/main/java/com/turbodocx/models/FieldConditional.java new file mode 100644 index 00000000..3854c749 --- /dev/null +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/FieldConditional.java @@ -0,0 +1,39 @@ +package com.turbodocx.models; + +import com.google.gson.annotations.SerializedName; + +/** + * Conditional (IF/THEN) rule set on a DEPENDENT field. + * + * The dependent field reacts to a CONTROLLING checkbox elsewhere in the same fields list. + * The controlling field must be type "checkbox" and carry FieldMetadata.fieldKey; this rule + * references it by that exact key. + * + * operator is "is_checked" or "is_not_checked". + * action is "show" (hidden until met) or "unlock" (visible but read-only until met). + */ +public class FieldConditional { + @SerializedName("controllingFieldKey") + private final String controllingFieldKey; + + @SerializedName("operator") + private final String operator; + + @SerializedName("action") + private final String action; + + /** + * @param controllingFieldKey must equal the controlling checkbox's FieldMetadata.fieldKey + * @param operator "is_checked" or "is_not_checked" + * @param action "show" or "unlock" + */ + public FieldConditional(String controllingFieldKey, String operator, String action) { + this.controllingFieldKey = controllingFieldKey; + this.operator = operator; + this.action = action; + } + + public String getControllingFieldKey() { return controllingFieldKey; } + public String getOperator() { return operator; } + public String getAction() { return action; } +} diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/FieldMetadata.java b/packages/java-sdk/src/main/java/com/turbodocx/models/FieldMetadata.java new file mode 100644 index 00000000..8daf7687 --- /dev/null +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/FieldMetadata.java @@ -0,0 +1,40 @@ +package com.turbodocx.models; + +import com.google.gson.annotations.SerializedName; + +/** + * Optional per-field metadata for conditional (IF/THEN) logic. + * + * Set fieldKey on a CONTROLLING checkbox to give it a stable client id; set conditional on a + * DEPENDENT field to make it react to that checkbox. Both sides are authored by the caller in + * the same payload. + */ +public class FieldMetadata { + @SerializedName("fieldKey") + private final String fieldKey; + + @SerializedName("conditional") + private final FieldConditional conditional; + + /** + * @param fieldKey stable client id (<=100 chars) for a controlling checkbox, referenced by dependents + * @param conditional conditional rule set on a dependent field + */ + public FieldMetadata(String fieldKey, FieldConditional conditional) { + this.fieldKey = fieldKey; + this.conditional = conditional; + } + + /** Convenience for a controlling checkbox that only carries a fieldKey. */ + public static FieldMetadata forFieldKey(String fieldKey) { + return new FieldMetadata(fieldKey, null); + } + + /** Convenience for a dependent field that only carries a conditional rule. */ + public static FieldMetadata forConditional(FieldConditional conditional) { + return new FieldMetadata(null, conditional); + } + + public String getFieldKey() { return fieldKey; } + public FieldConditional getConditional() { return conditional; } +} diff --git a/packages/java-sdk/src/test/java/com/turbodocx/TurboSignTest.java b/packages/java-sdk/src/test/java/com/turbodocx/TurboSignTest.java index 3ba83a07..8d19e5e1 100644 --- a/packages/java-sdk/src/test/java/com/turbodocx/TurboSignTest.java +++ b/packages/java-sdk/src/test/java/com/turbodocx/TurboSignTest.java @@ -682,4 +682,56 @@ void handleRateLimitError() { assertEquals(429, exception.getStatusCode()); } + + @Test + @DisplayName("should serialize conditional (IF/THEN) field metadata into the fields request part") + void sendSignatureSerializesConditionalMetadata() throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(gson.toJson(Map.of( + "success", true, + "documentId", "doc-conditional", + "status", "UNDER_REVIEW", + "message", "Document sent for signing" + )))); + + SendSignatureRequest request = new SendSignatureRequest.Builder() + .fileLink("https://example.com/doc.pdf") + .recipients(Collections.singletonList( + new Recipient("John Doe", "john@example.com", 1))) + .fields(Arrays.asList( + // Controlling checkbox + new Field.Builder() + .type("checkbox") + .recipientEmail("john@example.com") + .metadata(FieldMetadata.forFieldKey("request_changes")) + .build(), + // Dependent field + new Field.Builder() + .type("text") + .recipientEmail("john@example.com") + .metadata(FieldMetadata.forConditional( + new FieldConditional("request_changes", "is_checked", "show"))) + .build())) + .build(); + + SendSignatureResponse result = client.turboSign().sendSignature(request); + assertEquals("doc-conditional", result.getDocumentId()); + + // fields is JSON-stringified wholesale into the request body — metadata must ride along. + RecordedRequest recorded = server.takeRequest(); + Map body = gson.fromJson(recorded.getBody().readUtf8(), Map.class); + String fieldsJson = (String) body.get("fields"); + Field[] sentFields = gson.fromJson(fieldsJson, Field[].class); + + assertNotNull(sentFields[0].getMetadata()); + assertEquals("request_changes", sentFields[0].getMetadata().getFieldKey()); + assertNotNull(sentFields[1].getMetadata()); + FieldConditional conditional = sentFields[1].getMetadata().getConditional(); + assertNotNull(conditional); + assertEquals("request_changes", conditional.getControllingFieldKey()); + assertEquals("is_checked", conditional.getOperator()); + assertEquals("show", conditional.getAction()); + } } diff --git a/packages/java-sdk/src/test/java/com/turbodocx/models/FieldBuilderTest.java b/packages/java-sdk/src/test/java/com/turbodocx/models/FieldBuilderTest.java index d911b83d..8db840db 100644 --- a/packages/java-sdk/src/test/java/com/turbodocx/models/FieldBuilderTest.java +++ b/packages/java-sdk/src/test/java/com/turbodocx/models/FieldBuilderTest.java @@ -159,6 +159,56 @@ void buildFieldWithAllOptions() { assertNotNull(field.getTemplate()); } + // ============================================ + // Conditional (IF/THEN) metadata Tests + // ============================================ + + @Test + @DisplayName("should build a controlling checkbox carrying a metadata fieldKey") + void buildControllingCheckboxWithFieldKey() { + Field field = new Field.Builder() + .type("checkbox") + .recipientEmail("john@example.com") + .metadata(FieldMetadata.forFieldKey("request_changes")) + .build(); + + assertNotNull(field.getMetadata()); + assertEquals("request_changes", field.getMetadata().getFieldKey()); + assertNull(field.getMetadata().getConditional()); + } + + @Test + @DisplayName("should build a dependent field carrying a conditional rule") + void buildDependentFieldWithConditional() { + Field field = new Field.Builder() + .type("text") + .recipientEmail("john@example.com") + .isMultiline(true) + .metadata(FieldMetadata.forConditional( + new FieldConditional("request_changes", "is_checked", "show"))) + .build(); + + assertNotNull(field.getMetadata()); + assertNull(field.getMetadata().getFieldKey()); + FieldConditional conditional = field.getMetadata().getConditional(); + assertNotNull(conditional); + assertEquals("request_changes", conditional.getControllingFieldKey()); + assertEquals("is_checked", conditional.getOperator()); + assertEquals("show", conditional.getAction()); + } + + @Test + @DisplayName("should leave metadata null when not set") + void metadataNullByDefault() { + Field field = new Field.Builder() + .type("signature") + .page(1).x(100).y(500).width(200).height(50) + .recipientEmail("john@example.com") + .build(); + + assertNull(field.getMetadata()); + } + // ============================================ // Field.TemplateAnchor.Builder Tests // ============================================ diff --git a/packages/js-sdk/README.md b/packages/js-sdk/README.md index e7f85401..8cf1848c 100644 --- a/packages/js-sdk/README.md +++ b/packages/js-sdk/README.md @@ -844,6 +844,8 @@ console.log(quote.preparedBy?.email); // may be undefined for an API-created qu | `title` | Job title | | `company` | Company name | +The `checkbox` type doubles as the **controlling** field for conditional logic (see below). + ### Field Positioning ```typescript @@ -859,6 +861,45 @@ console.log(quote.preparedBy?.email); // may be undefined for an API-created qu } ``` +### Conditional (IF/THEN) Fields + +Any field may carry an optional `metadata` object that drives conditional logic. Set +`metadata.fieldKey` on a **controlling** `checkbox` to give it a stable id, then set +`metadata.conditional` on a **dependent** field that references that id: + +```typescript +fields: [ + // Controlling checkbox — carries the fieldKey dependents reference + { + type: 'checkbox', + recipientEmail: 'john@example.com', + template: { anchor: '{request_changes}', placement: 'replace', size: { width: 20, height: 20 } }, + metadata: { fieldKey: 'request_changes' } + }, + // Dependent text field — hidden until the checkbox is checked ("If checked, explain") + { + type: 'text', + recipientEmail: 'john@example.com', + isMultiline: true, + template: { anchor: '{change_details}', placement: 'replace', size: { width: 200, height: 50 } }, + metadata: { + conditional: { + controllingFieldKey: 'request_changes', // must equal the checkbox's metadata.fieldKey + operator: 'is_checked', // 'is_checked' | 'is_not_checked' + action: 'show' // 'show' (hidden until met) | 'unlock' (read-only until met) + } + } + } +] +``` + +| `metadata` field | Set on | Meaning | +|:-----------------|:-------|:--------| +| `fieldKey` | controlling `checkbox` | Stable client id (≤100 chars) that dependents reference | +| `conditional.controllingFieldKey` | dependent field | Must equal the controlling checkbox's `fieldKey` | +| `conditional.operator` | dependent field | `is_checked` or `is_not_checked` | +| `conditional.action` | dependent field | `show` (hidden until met) or `unlock` (visible but read-only until met) | + --- ## Examples diff --git a/packages/js-sdk/examples/turbosign-advanced.ts b/packages/js-sdk/examples/turbosign-advanced.ts index d961b613..043e1220 100644 --- a/packages/js-sdk/examples/turbosign-advanced.ts +++ b/packages/js-sdk/examples/turbosign-advanced.ts @@ -118,6 +118,38 @@ async function advancedFieldsExample() { placement: 'replace', size: { width: 200, height: 50 } } + }, + + // ── Conditional (IF/THEN) fields ───────────────────────────── + // Controlling checkbox: carries a stable `fieldKey` that dependents reference. + { + type: 'checkbox', + recipientEmail: 'john@example.com', + template: { + anchor: '{request_changes}', + placement: 'replace', + size: { width: 20, height: 20 } + }, + metadata: { fieldKey: 'request_changes' } + }, + + // Dependent text field: hidden until the checkbox above is checked ("If checked, explain"). + { + type: 'text', + recipientEmail: 'john@example.com', + isMultiline: true, + template: { + anchor: '{change_details}', + placement: 'replace', + size: { width: 200, height: 50 } + }, + metadata: { + conditional: { + controllingFieldKey: 'request_changes', + operator: 'is_checked', + action: 'show' + } + } } ] }); diff --git a/packages/js-sdk/src/types/sign.ts b/packages/js-sdk/src/types/sign.ts index f8f2ecdf..de91fa7d 100644 --- a/packages/js-sdk/src/types/sign.ts +++ b/packages/js-sdk/src/types/sign.ts @@ -297,6 +297,50 @@ export interface DocumentRecipientsResponse { // SINGLE-STEP OPERATION TYPES // ============================================ +/** + * How a dependent field's controlling checkbox is tested. + * - `is_checked` — the condition is met while the controlling checkbox is checked + * - `is_not_checked` — the condition is met while the controlling checkbox is unchecked + */ +export type ConditionalOperator = 'is_checked' | 'is_not_checked'; + +/** + * What happens to a dependent field until its condition is met. + * - `show` — the field is hidden until the condition is met, then revealed + * - `unlock` — the field is visible but read-only until the condition is met, then editable + */ +export type ConditionalAction = 'show' | 'unlock'; + +/** + * Conditional (IF/THEN) rule set on a DEPENDENT field. + * + * The dependent field reacts to a CONTROLLING checkbox elsewhere in the same `fields` array. + * The controlling field must be `type: 'checkbox'` and carry `metadata.fieldKey`; this rule + * references it by that exact key. + */ +export interface FieldConditional { + /** Must equal the controlling checkbox's `metadata.fieldKey`. */ + controllingFieldKey: string; + /** Whether the rule fires when the controlling checkbox is checked or unchecked. */ + operator: ConditionalOperator; + /** Whether the dependent field is hidden (`show`) or read-only (`unlock`) until met. */ + action: ConditionalAction; +} + +/** + * Optional per-field metadata for conditional (IF/THEN) logic. + * + * Set `fieldKey` on a CONTROLLING checkbox to give it a stable client id; set `conditional` + * on a DEPENDENT field to make it react to that checkbox. Both sides are authored by the + * caller in the same payload. + */ +export interface FieldMetadata { + /** Stable client id (≤100 chars) for a CONTROLLING checkbox, referenced by dependents. */ + fieldKey?: string; + /** Conditional rule set on a DEPENDENT field. */ + conditional?: FieldConditional; +} + /** * Field configuration for single-step operations * Supports both coordinate-based and template anchor-based positioning @@ -343,6 +387,11 @@ export interface Field { /** Use regex for anchor/searchText (default: false) */ useRegex?: boolean; }; + /** + * Optional metadata for conditional (IF/THEN) logic. Set `fieldKey` on a controlling + * checkbox; set `conditional` on a dependent field that reacts to it. + */ + metadata?: FieldMetadata; } /** diff --git a/packages/js-sdk/tests/turbosign.test.ts b/packages/js-sdk/tests/turbosign.test.ts index ede3b2e8..b3ed2662 100644 --- a/packages/js-sdk/tests/turbosign.test.ts +++ b/packages/js-sdk/tests/turbosign.test.ts @@ -406,6 +406,71 @@ describe("TurboSign Module", () => { expect(result.documentId).toBe("doc-checkbox"); }); + + it("should serialize conditional (IF/THEN) field metadata into the fields request part", async () => { + const mockResponse = { + success: true, + documentId: "doc-conditional", + status: "UNDER_REVIEW", + recipients: [ + { id: "r-1", name: "John Doe", email: "john@example.com", metadata: {} }, + ], + message: "Document sent for signing", + }; + + // Controlling checkbox carries metadata.fieldKey; dependent field references it. + const conditionalFields: Field[] = [ + { + type: "checkbox", + page: 1, + x: 100, + y: 600, + width: 20, + height: 20, + recipientEmail: "john@example.com", + metadata: { fieldKey: "request_changes" }, + }, + { + type: "text", + page: 1, + x: 100, + y: 650, + width: 200, + height: 50, + recipientEmail: "john@example.com", + metadata: { + conditional: { + controllingFieldKey: "request_changes", + operator: "is_checked", + action: "show", + }, + }, + }, + ]; + + MockedHttpClient.prototype.post = jest + .fn() + .mockResolvedValue(mockResponse); + TurboSign.configure({ apiKey: "test-key" }); + + await TurboSign.sendSignature({ + fileLink: "https://example.com/doc.pdf", + recipients: mockRecipients, + fields: conditionalFields, + }); + + // fields is JSON-stringified wholesale into the request body — metadata must ride along. + const [, formData] = (MockedHttpClient.prototype.post as jest.Mock).mock + .calls[0]; + const sentFields = JSON.parse(formData.fields); + + expect(sentFields[0].metadata).toEqual({ fieldKey: "request_changes" }); + expect(sentFields[1].metadata.conditional).toEqual({ + controllingFieldKey: "request_changes", + operator: "is_checked", + action: "show", + }); + }); }); describe("getStatus", () => { diff --git a/packages/php-sdk/README.md b/packages/php-sdk/README.md index 0b6376af..a95865b3 100644 --- a/packages/php-sdk/README.md +++ b/packages/php-sdk/README.md @@ -585,9 +585,57 @@ SignatureFieldType::LAST_NAME // Last name SignatureFieldType::EMAIL // Email address SignatureFieldType::TITLE // Job title SignatureFieldType::COMPANY // Company name -SignatureFieldType::CHECKBOX // Checkbox field +SignatureFieldType::CHECKBOX // Checkbox field (also the controlling field for conditional logic) ``` +### Conditional (IF/THEN) Fields + +Any field may carry an optional `FieldMetadata` that drives conditional logic. Set a `fieldKey` on +a **controlling** `CHECKBOX` to give it a stable id, then set a `FieldConditional` on a +**dependent** field that references that id: + +```php +use TurboDocx\Types\Field; +use TurboDocx\Types\SignatureFieldType; +use TurboDocx\Types\TemplateConfig; +use TurboDocx\Types\FieldPlacement; +use TurboDocx\Types\FieldMetadata; +use TurboDocx\Types\FieldConditional; +use TurboDocx\Types\ConditionalOperator; +use TurboDocx\Types\ConditionalAction; + +$fields = [ + // Controlling checkbox — carries the fieldKey dependents reference + new Field( + type: SignatureFieldType::CHECKBOX, + recipientEmail: 'john@example.com', + template: new TemplateConfig(anchor: '{request_changes}', placement: FieldPlacement::REPLACE, size: ['width' => 20, 'height' => 20]), + metadata: new FieldMetadata(fieldKey: 'request_changes') + ), + // Dependent text field — hidden until the checkbox is checked ("If checked, explain") + new Field( + type: SignatureFieldType::TEXT, + recipientEmail: 'john@example.com', + isMultiline: true, + template: new TemplateConfig(anchor: '{change_details}', placement: FieldPlacement::REPLACE, size: ['width' => 200, 'height' => 50]), + metadata: new FieldMetadata( + conditional: new FieldConditional( + controllingFieldKey: 'request_changes', // must equal the checkbox's fieldKey + operator: ConditionalOperator::IS_CHECKED, // IS_CHECKED | IS_NOT_CHECKED + action: ConditionalAction::SHOW // SHOW (hidden until met) | UNLOCK (read-only until met) + ) + ) + ), +]; +``` + +| `FieldMetadata` field | Set on | Meaning | +|:----------------------|:-------|:--------| +| `fieldKey` | controlling `CHECKBOX` | Stable client id (≤100 chars) that dependents reference | +| `conditional->controllingFieldKey` | dependent field | Must equal the controlling checkbox's `fieldKey` | +| `conditional->operator` | dependent field | `ConditionalOperator::IS_CHECKED` (`"is_checked"`) or `IS_NOT_CHECKED` (`"is_not_checked"`) | +| `conditional->action` | dependent field | `ConditionalAction::SHOW` (hidden until met) or `UNLOCK` (visible but read-only until met) | + ### Field Positioning TurboSign supports two ways to position fields: diff --git a/packages/php-sdk/examples/turbosign-advanced.php b/packages/php-sdk/examples/turbosign-advanced.php index a9400c62..be20e7e9 100644 --- a/packages/php-sdk/examples/turbosign-advanced.php +++ b/packages/php-sdk/examples/turbosign-advanced.php @@ -23,6 +23,10 @@ use TurboDocx\Types\SignatureFieldType; use TurboDocx\Types\TemplateConfig; use TurboDocx\Types\FieldPlacement; +use TurboDocx\Types\FieldMetadata; +use TurboDocx\Types\FieldConditional; +use TurboDocx\Types\ConditionalOperator; +use TurboDocx\Types\ConditionalAction; use TurboDocx\Types\Requests\CreateSignatureReviewLinkRequest; function advancedFieldsExample(): void @@ -126,6 +130,38 @@ function advancedFieldsExample(): void size: ['width' => 200, 'height' => 50] ) ), + + // Conditional (IF/THEN) fields + // Controlling checkbox: carries a stable fieldKey that dependents reference + new Field( + type: SignatureFieldType::CHECKBOX, + recipientEmail: 'john@example.com', + template: new TemplateConfig( + anchor: '{request_changes}', + placement: FieldPlacement::REPLACE, + size: ['width' => 20, 'height' => 20] + ), + metadata: new FieldMetadata(fieldKey: 'request_changes') + ), + + // Dependent text field: hidden until the checkbox above is checked ("If checked, explain") + new Field( + type: SignatureFieldType::TEXT, + recipientEmail: 'john@example.com', + isMultiline: true, + template: new TemplateConfig( + anchor: '{change_details}', + placement: FieldPlacement::REPLACE, + size: ['width' => 200, 'height' => 50] + ), + metadata: new FieldMetadata( + conditional: new FieldConditional( + controllingFieldKey: 'request_changes', + operator: ConditionalOperator::IS_CHECKED, + action: ConditionalAction::SHOW + ) + ) + ), ], file: $pdfFile, documentName: 'Advanced Contract', diff --git a/packages/php-sdk/src/Types/ConditionalAction.php b/packages/php-sdk/src/Types/ConditionalAction.php new file mode 100644 index 00000000..6ed99afe --- /dev/null +++ b/packages/php-sdk/src/Types/ConditionalAction.php @@ -0,0 +1,16 @@ +backgroundColor; } + // Add conditional (IF/THEN) metadata + if ($this->metadata !== null) { + $data['metadata'] = $this->metadata->toArray(); + } + return $data; } } diff --git a/packages/php-sdk/src/Types/FieldConditional.php b/packages/php-sdk/src/Types/FieldConditional.php new file mode 100644 index 00000000..76307a84 --- /dev/null +++ b/packages/php-sdk/src/Types/FieldConditional.php @@ -0,0 +1,40 @@ + + */ + public function toArray(): array + { + return [ + 'controllingFieldKey' => $this->controllingFieldKey, + 'operator' => $this->operator->value, + 'action' => $this->action->value, + ]; + } +} diff --git a/packages/php-sdk/src/Types/FieldMetadata.php b/packages/php-sdk/src/Types/FieldMetadata.php new file mode 100644 index 00000000..b01f998c --- /dev/null +++ b/packages/php-sdk/src/Types/FieldMetadata.php @@ -0,0 +1,43 @@ + + */ + public function toArray(): array + { + $data = []; + + if ($this->fieldKey !== null) { + $data['fieldKey'] = $this->fieldKey; + } + if ($this->conditional !== null) { + $data['conditional'] = $this->conditional->toArray(); + } + + return $data; + } +} diff --git a/packages/php-sdk/tests/Unit/FieldTest.php b/packages/php-sdk/tests/Unit/FieldTest.php index 70b81e88..7a078ba5 100644 --- a/packages/php-sdk/tests/Unit/FieldTest.php +++ b/packages/php-sdk/tests/Unit/FieldTest.php @@ -5,7 +5,11 @@ namespace TurboDocx\Tests\Unit; use PHPUnit\Framework\TestCase; +use TurboDocx\Types\ConditionalAction; +use TurboDocx\Types\ConditionalOperator; use TurboDocx\Types\Field; +use TurboDocx\Types\FieldConditional; +use TurboDocx\Types\FieldMetadata; use TurboDocx\Types\FieldPlacement; use TurboDocx\Types\SignatureFieldType; use TurboDocx\Types\TemplateConfig; @@ -143,5 +147,49 @@ public function testToArrayExcludesNullOptionalProperties(): void $this->assertArrayNotHasKey('required', $array); $this->assertArrayNotHasKey('backgroundColor', $array); $this->assertArrayNotHasKey('template', $array); + $this->assertArrayNotHasKey('metadata', $array); + } + + public function testToArrayWithControllingCheckboxFieldKey(): void + { + // A controlling checkbox carries a stable fieldKey that dependents reference. + $field = new Field( + type: SignatureFieldType::CHECKBOX, + recipientEmail: 'john@example.com', + metadata: new FieldMetadata(fieldKey: 'request_changes') + ); + + $array = $field->toArray(); + + $this->assertArrayHasKey('metadata', $array); + $this->assertEquals(['fieldKey' => 'request_changes'], $array['metadata']); + } + + public function testToArrayWithDependentConditional(): void + { + // A dependent field references the controlling checkbox by its fieldKey. + $field = new Field( + type: SignatureFieldType::TEXT, + recipientEmail: 'john@example.com', + isMultiline: true, + metadata: new FieldMetadata( + conditional: new FieldConditional( + controllingFieldKey: 'request_changes', + operator: ConditionalOperator::IS_CHECKED, + action: ConditionalAction::SHOW + ) + ) + ); + + $array = $field->toArray(); + + $this->assertArrayHasKey('metadata', $array); + $this->assertEquals([ + 'conditional' => [ + 'controllingFieldKey' => 'request_changes', + 'operator' => 'is_checked', + 'action' => 'show', + ], + ], $array['metadata']); } } diff --git a/packages/py-sdk/README.md b/packages/py-sdk/README.md index 6ce98ad6..7c205b9b 100644 --- a/packages/py-sdk/README.md +++ b/packages/py-sdk/README.md @@ -804,6 +804,48 @@ print(prepared.get("email")) # may be absent for an API-created quote — rende | `title` | Job title | | `company` | Company name | +The `checkbox` type doubles as the **controlling** field for conditional logic (see below). + +### Conditional (IF/THEN) Fields + +Any field dict may carry an optional `metadata` key that drives conditional logic. Set +`metadata.fieldKey` on a **controlling** `checkbox` to give it a stable id, then set +`metadata.conditional` on a **dependent** field that references that id. Field dicts are passed +through verbatim, so the keys stay **camelCase**: + +```python +fields = [ + # Controlling checkbox — carries the fieldKey dependents reference + { + "type": "checkbox", + "recipientEmail": "john@example.com", + "template": {"anchor": "{request_changes}", "placement": "replace", "size": {"width": 20, "height": 20}}, + "metadata": {"fieldKey": "request_changes"}, + }, + # Dependent text field — hidden until the checkbox is checked ("If checked, explain") + { + "type": "text", + "recipientEmail": "john@example.com", + "isMultiline": True, + "template": {"anchor": "{change_details}", "placement": "replace", "size": {"width": 200, "height": 50}}, + "metadata": { + "conditional": { + "controllingFieldKey": "request_changes", # must equal the checkbox's fieldKey + "operator": "is_checked", # "is_checked" | "is_not_checked" + "action": "show", # "show" (hidden until met) | "unlock" (read-only until met) + } + }, + }, +] +``` + +| `metadata` key | Set on | Meaning | +|:---------------|:-------|:--------| +| `fieldKey` | controlling `checkbox` | Stable client id (≤100 chars) that dependents reference | +| `conditional.controllingFieldKey` | dependent field | Must equal the controlling checkbox's `fieldKey` | +| `conditional.operator` | dependent field | `"is_checked"` or `"is_not_checked"` | +| `conditional.action` | dependent field | `"show"` (hidden until met) or `"unlock"` (visible but read-only until met) | + --- ## Examples diff --git a/packages/py-sdk/examples/turbosign_advanced.py b/packages/py-sdk/examples/turbosign_advanced.py index dbf551b8..2e98079b 100644 --- a/packages/py-sdk/examples/turbosign_advanced.py +++ b/packages/py-sdk/examples/turbosign_advanced.py @@ -116,6 +116,36 @@ async def advanced_fields_example(): "placement": "replace", "size": {"width": 200, "height": 50} } + }, + # Conditional (IF/THEN) fields + # Controlling checkbox: carries a stable fieldKey that dependents reference + { + "type": "checkbox", + "recipientEmail": "john@example.com", + "template": { + "anchor": "{request_changes}", + "placement": "replace", + "size": {"width": 20, "height": 20} + }, + "metadata": {"fieldKey": "request_changes"} + }, + # Dependent text field: hidden until the checkbox above is checked ("If checked, explain") + { + "type": "text", + "recipientEmail": "john@example.com", + "isMultiline": True, + "template": { + "anchor": "{change_details}", + "placement": "replace", + "size": {"width": 200, "height": 50} + }, + "metadata": { + "conditional": { + "controllingFieldKey": "request_changes", + "operator": "is_checked", + "action": "show" + } + } } ] ) diff --git a/packages/py-sdk/src/turbodocx_sdk/modules/sign.py b/packages/py-sdk/src/turbodocx_sdk/modules/sign.py index 429b40dc..bdb4af44 100644 --- a/packages/py-sdk/src/turbodocx_sdk/modules/sign.py +++ b/packages/py-sdk/src/turbodocx_sdk/modules/sign.py @@ -163,6 +163,13 @@ async def create_signature_review_link( Each recipient should have: name, email, signingOrder fields: Signature fields configuration Each field should have: type, recipientEmail, and positioning info + Optional per-field "metadata" drives conditional (IF/THEN) logic: + - On a controlling checkbox: {"metadata": {"fieldKey": "request_changes"}} + - On a dependent field: {"metadata": {"conditional": { + "controllingFieldKey": "request_changes", # must equal the checkbox's fieldKey + "operator": "is_checked" | "is_not_checked", + "action": "show" | "unlock"}}} # show = hidden until met; unlock = read-only until met + Field dicts are passed through verbatim, so keys stay camelCase. file: PDF file content as bytes file_name: Original filename file_link: URL to document file @@ -310,6 +317,13 @@ async def send_signature( Each recipient should have: name, email, signingOrder fields: Signature fields configuration Each field should have: type, recipientEmail, and positioning info + Optional per-field "metadata" drives conditional (IF/THEN) logic: + - On a controlling checkbox: {"metadata": {"fieldKey": "request_changes"}} + - On a dependent field: {"metadata": {"conditional": { + "controllingFieldKey": "request_changes", # must equal the checkbox's fieldKey + "operator": "is_checked" | "is_not_checked", + "action": "show" | "unlock"}}} # show = hidden until met; unlock = read-only until met + Field dicts are passed through verbatim, so keys stay camelCase. file: PDF file content as bytes file_name: Original filename file_link: URL to document file diff --git a/packages/py-sdk/tests/test_turbosign.py b/packages/py-sdk/tests/test_turbosign.py index f2c30f8f..a059cfe9 100644 --- a/packages/py-sdk/tests/test_turbosign.py +++ b/packages/py-sdk/tests/test_turbosign.py @@ -11,6 +11,7 @@ - get_audit_trail """ +import json import pytest from unittest.mock import AsyncMock, MagicMock, patch from turbodocx_sdk import TurboSign, ValidationError, NotFoundError, AuthenticationError @@ -211,6 +212,59 @@ async def test_create_signature_review_link_with_optional_fields(self): assert data.get("senderName") == "Sales Team" assert data.get("senderEmail") == "sales@company.com" + @pytest.mark.asyncio + async def test_conditional_field_metadata_serializes(self): + """Should serialize conditional (IF/THEN) field metadata into the fields request part""" + mock_response = { + "success": True, + "documentId": "doc-conditional", + "status": "review_ready", + "message": "Document prepared for review" + } + + conditional_fields = [ + # Controlling checkbox carries metadata.fieldKey + { + "type": "checkbox", + "recipientEmail": "john@example.com", + "metadata": {"fieldKey": "request_changes"} + }, + # Dependent field references it + { + "type": "text", + "recipientEmail": "john@example.com", + "metadata": { + "conditional": { + "controllingFieldKey": "request_changes", + "operator": "is_checked", + "action": "show" + } + } + } + ] + + with patch.object(TurboSign, '_get_client') as mock_get_client: + mock_client = MagicMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_get_client.return_value = mock_client + + TurboSign.configure(api_key="test-key", org_id="test-org", sender_email="test@example.com") + await TurboSign.create_signature_review_link( + file_link="https://example.com/doc.pdf", + recipients=self.mock_recipients(), + fields=conditional_fields + ) + + # fields is json.dumps'd wholesale into the request body — metadata must ride along. + data = mock_client.post.call_args[1]["data"] + sent_fields = json.loads(data["fields"]) + assert sent_fields[0]["metadata"] == {"fieldKey": "request_changes"} + assert sent_fields[1]["metadata"]["conditional"] == { + "controllingFieldKey": "request_changes", + "operator": "is_checked", + "action": "show" + } + class TestSendSignature: """Test send_signature operation""" diff --git a/packages/ruby-sdk/README.md b/packages/ruby-sdk/README.md index c5c0f1e4..7e5fd1be 100644 --- a/packages/ruby-sdk/README.md +++ b/packages/ruby-sdk/README.md @@ -1070,6 +1070,48 @@ Non-admin callers receive `TurboDocxSdk::AuthorizationError`. | `title` | Job title | | `company` | Company name | +The `checkbox` type doubles as the **controlling** field for conditional logic (see below). + +### Conditional (IF/THEN) Fields + +Any field hash may carry an optional `"metadata"` key that drives conditional logic. Set +`metadata.fieldKey` on a **controlling** `checkbox` to give it a stable id, then set +`metadata.conditional` on a **dependent** field that references that id. Field hashes are passed +through verbatim, so the keys stay **camelCase**: + +```ruby +"fields" => [ + # Controlling checkbox — carries the fieldKey dependents reference + { + "type" => "checkbox", + "recipientEmail" => "john@example.com", + "template" => { "anchor" => "{request_changes}", "placement" => "replace", "size" => { "width" => 20, "height" => 20 } }, + "metadata" => { "fieldKey" => "request_changes" } + }, + # Dependent text field — hidden until the checkbox is checked ("If checked, explain") + { + "type" => "text", + "recipientEmail" => "john@example.com", + "isMultiline" => true, + "template" => { "anchor" => "{change_details}", "placement" => "replace", "size" => { "width" => 200, "height" => 50 } }, + "metadata" => { + "conditional" => { + "controllingFieldKey" => "request_changes", # must equal the checkbox's fieldKey + "operator" => "is_checked", # "is_checked" | "is_not_checked" + "action" => "show" # "show" (hidden until met) | "unlock" (read-only until met) + } + } + } +] +``` + +| `metadata` key | Set on | Meaning | +|:---------------|:-------|:--------| +| `fieldKey` | controlling `checkbox` | Stable client id (≤100 chars) that dependents reference | +| `conditional.controllingFieldKey` | dependent field | Must equal the controlling checkbox's `fieldKey` | +| `conditional.operator` | dependent field | `"is_checked"` or `"is_not_checked"` | +| `conditional.action` | dependent field | `"show"` (hidden until met) or `"unlock"` (visible but read-only until met) | + --- ## Constants diff --git a/packages/ruby-sdk/examples/turbosign_advanced.rb b/packages/ruby-sdk/examples/turbosign_advanced.rb index 55c9f7ee..86cc7aec 100644 --- a/packages/ruby-sdk/examples/turbosign_advanced.rb +++ b/packages/ruby-sdk/examples/turbosign_advanced.rb @@ -119,6 +119,36 @@ "placement" => "replace", "size" => { "width" => 200, "height" => 50 } } + }, + # Conditional (IF/THEN) fields + # Controlling checkbox: carries a stable fieldKey that dependents reference + { + "type" => "checkbox", + "recipientEmail" => "john@example.com", + "template" => { + "anchor" => "{request_changes}", + "placement" => "replace", + "size" => { "width" => 20, "height" => 20 } + }, + "metadata" => { "fieldKey" => "request_changes" } + }, + # Dependent text field: hidden until the checkbox above is checked ("If checked, explain") + { + "type" => "text", + "recipientEmail" => "john@example.com", + "isMultiline" => true, + "template" => { + "anchor" => "{change_details}", + "placement" => "replace", + "size" => { "width" => 200, "height" => 50 } + }, + "metadata" => { + "conditional" => { + "controllingFieldKey" => "request_changes", + "operator" => "is_checked", + "action" => "show" + } + } } ] ) diff --git a/packages/ruby-sdk/spec/turbo_sign_spec.rb b/packages/ruby-sdk/spec/turbo_sign_spec.rb index b476c1ae..9ac69bd5 100644 --- a/packages/ruby-sdk/spec/turbo_sign_spec.rb +++ b/packages/ruby-sdk/spec/turbo_sign_spec.rb @@ -166,6 +166,55 @@ ) end + it "serializes conditional (IF/THEN) field metadata into the fields request part" do + mock_response = { + "success" => true, + "documentId" => "doc-conditional", + "status" => "review_ready", + "message" => "Document prepared for review" + } + + captured_data = nil + allow(mock_client).to receive(:post) do |_path, data| + captured_data = data + mock_response + end + + described_class.create_signature_review_link( + "fileLink" => "https://example.com/doc.pdf", + "recipients" => [{ "name" => "John Doe", "email" => "john@example.com", "signingOrder" => 1 }], + "fields" => [ + # Controlling checkbox carries metadata.fieldKey + { + "type" => "checkbox", + "recipientEmail" => "john@example.com", + "metadata" => { "fieldKey" => "request_changes" } + }, + # Dependent field references it + { + "type" => "text", + "recipientEmail" => "john@example.com", + "metadata" => { + "conditional" => { + "controllingFieldKey" => "request_changes", + "operator" => "is_checked", + "action" => "show" + } + } + } + ] + ) + + # fields is JSON.generate'd wholesale into the request body -- metadata must ride along. + sent_fields = JSON.parse(captured_data["fields"]) + expect(sent_fields[0]["metadata"]).to eq("fieldKey" => "request_changes") + expect(sent_fields[1]["metadata"]["conditional"]).to eq( + "controllingFieldKey" => "request_changes", + "operator" => "is_checked", + "action" => "show" + ) + end + it "prepares document for review with deliverable ID" do mock_response = { "success" => true, From e9f7bbba621e42ea866dc30eb0c4f052305c7883 Mon Sep 17 00:00:00 2001 From: Amit Sharma Date: Wed, 19 Aug 2026 19:06:54 +0530 Subject: [PATCH 2/3] docs(examples): add a conditional (IF/THEN) fields example to all six SDKs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dedicated, runnable example per language (JS/TS, Python, Go, PHP, Java, Ruby) showing a checkbox that controls other fields: three controlling checkboxes drive four dependents across show/unlock and is_checked/is_not_checked, including one checkbox driving two dependents, plus a plain required signature. Uses createSignatureReviewLink (no emails sent) and notes the InvalidConditionalRule 400 and the fail-open behavior. All six author the identical wire shape — metadata.{fieldKey, conditional{controllingFieldKey, operator, action}} — verified against the JS SDK run end-to-end against the API. JS typechecks and Python compiles locally; the typed SDKs are written to their real types (Go structs, PHP enums, Java FieldMetadata factories) and build in CI. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../examples/turbosign_conditional_fields.go | 98 +++++++++++++++ .../examples/TurboSignConditionalFields.java | 92 ++++++++++++++ .../examples/turbosign-conditional-fields.ts | 117 ++++++++++++++++++ .../examples/turbosign-conditional-fields.php | 102 +++++++++++++++ .../examples/turbosign_conditional_fields.py | 82 ++++++++++++ .../examples/turbosign_conditional_fields.rb | 77 ++++++++++++ 6 files changed, 568 insertions(+) create mode 100644 packages/go-sdk/examples/turbosign_conditional_fields.go create mode 100644 packages/java-sdk/examples/TurboSignConditionalFields.java create mode 100644 packages/js-sdk/examples/turbosign-conditional-fields.ts create mode 100644 packages/php-sdk/examples/turbosign-conditional-fields.php create mode 100644 packages/py-sdk/examples/turbosign_conditional_fields.py create mode 100644 packages/ruby-sdk/examples/turbosign_conditional_fields.rb diff --git a/packages/go-sdk/examples/turbosign_conditional_fields.go b/packages/go-sdk/examples/turbosign_conditional_fields.go new file mode 100644 index 00000000..84dfa0d7 --- /dev/null +++ b/packages/go-sdk/examples/turbosign_conditional_fields.go @@ -0,0 +1,98 @@ +//go:build ignore +// +build ignore + +// Example: Conditional (IF/THEN) Fields +// +// A checkbox can control other fields so signers only see what applies to them: +// - Give a "checkbox" field a stable Metadata.FieldKey. +// - Give a dependent field a Metadata.Conditional rule that references that key. +// Operator: "is_checked" | "is_not_checked" -- when the rule fires. +// Action: "show" (hidden until the rule fires) +// "unlock" (visible but read-only until the rule fires). +// +// One checkbox can drive any number of dependent fields -- give them the same +// ControllingFieldKey. Uses CreateSignatureReviewLink (no emails are sent). + +package main + +import ( + "context" + "fmt" + "os" + + turbodocx "github.com/TurboDocx/SDK/packages/go-sdk" +) + +func main() { + client, err := turbodocx.NewClientWithConfig(turbodocx.ClientConfig{ + APIKey: getEnv("TURBODOCX_API_KEY", "your-api-key-here"), + OrgID: getEnv("TURBODOCX_ORG_ID", "your-org-id-here"), + SenderEmail: getEnv("TURBODOCX_SENDER_EMAIL", "support@yourcompany.com"), + SenderName: getEnv("TURBODOCX_SENDER_NAME", "Your Company Name"), + }) + if err != nil { + fmt.Printf("Error creating client: %v\n", err) + return + } + + pdfFile, err := os.ReadFile("../../ExampleAssets/advanced-contract.pdf") + if err != nil { + fmt.Printf("Error reading file: %v\n", err) + return + } + + fmt.Println("Creating a review link with conditional fields...") + + ctx := context.Background() + result, err := client.TurboSign.CreateSignatureReviewLink(ctx, &turbodocx.CreateSignatureReviewLinkRequest{ + File: pdfFile, + FileName: "advanced-contract.pdf", + DocumentName: "Conditional Fields Demo", + Recipients: []turbodocx.Recipient{ + {Name: "John Doe", Email: "john@example.com", SigningOrder: 1}, + }, + Fields: []turbodocx.Field{ + // Controlling checkboxes -- each carries a stable FieldKey. + {Type: "checkbox", RecipientEmail: "john@example.com", Page: 1, X: 60, Y: 120, Width: 20, Height: 20, Metadata: &turbodocx.FieldMetadata{FieldKey: "request_changes"}}, + {Type: "checkbox", RecipientEmail: "john@example.com", Page: 1, X: 60, Y: 300, Width: 20, Height: 20, Metadata: &turbodocx.FieldMetadata{FieldKey: "override_amount"}}, + {Type: "checkbox", RecipientEmail: "john@example.com", Page: 1, X: 60, Y: 480, Width: 20, Height: 20, Metadata: &turbodocx.FieldMetadata{FieldKey: "consent"}}, + + // show + is_checked -- HIDDEN until "request_changes" is checked. + {Type: "text", RecipientEmail: "john@example.com", Page: 1, X: 120, Y: 120, Width: 260, Height: 40, + Metadata: &turbodocx.FieldMetadata{Conditional: &turbodocx.FieldConditional{ControllingFieldKey: "request_changes", Operator: "is_checked", Action: "show"}}}, + // ONE checkbox driving a SECOND dependent (same ControllingFieldKey) -- a signature. + {Type: "signature", RecipientEmail: "john@example.com", Page: 1, X: 120, Y: 180, Width: 200, Height: 50, + Metadata: &turbodocx.FieldMetadata{Conditional: &turbodocx.FieldConditional{ControllingFieldKey: "request_changes", Operator: "is_checked", Action: "show"}}}, + + // unlock + is_checked -- VISIBLE but locked until "override_amount" is checked. + {Type: "text", RecipientEmail: "john@example.com", Page: 1, X: 120, Y: 300, Width: 150, Height: 30, DefaultValue: "1000.00", + Metadata: &turbodocx.FieldMetadata{Conditional: &turbodocx.FieldConditional{ControllingFieldKey: "override_amount", Operator: "is_checked", Action: "unlock"}}}, + + // show + is_not_checked -- a "please explain" box shown only while consent is WITHHELD. + {Type: "text", RecipientEmail: "john@example.com", Page: 1, X: 120, Y: 480, Width: 260, Height: 40, + Metadata: &turbodocx.FieldMetadata{Conditional: &turbodocx.FieldConditional{ControllingFieldKey: "consent", Operator: "is_not_checked", Action: "show"}}}, + + // A normal required signature with no rule -- always visible, always required. + {Type: "signature", RecipientEmail: "john@example.com", Page: 1, X: 120, Y: 620, Width: 200, Height: 50, Required: true}, + }, + }) + if err != nil { + // A malformed rule is rejected here with a 400 and code "InvalidConditionalRule". + fmt.Printf("Error: %v\n", err) + return + } + + fmt.Println("✅ Review link created!") + fmt.Printf("Document ID: %s\n", result.DocumentID) + fmt.Printf("Preview URL: %s\n", result.PreviewURL) + + // Fail-open: a well-formed rule whose ControllingFieldKey matches NO checkbox is NOT an + // error -- the dependent field stays visible/editable. Double-check your keys match exactly. +} + +func getEnv(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} diff --git a/packages/java-sdk/examples/TurboSignConditionalFields.java b/packages/java-sdk/examples/TurboSignConditionalFields.java new file mode 100644 index 00000000..b1e8f2ce --- /dev/null +++ b/packages/java-sdk/examples/TurboSignConditionalFields.java @@ -0,0 +1,92 @@ +/** + * Example: Conditional (IF/THEN) Fields + * + * A checkbox can control other fields so signers only see what applies to them: + * - Give a "checkbox" field a stable metadata via FieldMetadata.forFieldKey("..."). + * - Give a dependent field FieldMetadata.forConditional(new FieldConditional(key, operator, action)). + * operator: "is_checked" | "is_not_checked" -- when the rule fires. + * action: "show" (hidden until the rule fires) + * "unlock" (visible but read-only until the rule fires). + * + * One checkbox can drive any number of dependent fields -- give them the same + * controllingFieldKey. Uses createSignatureReviewLink (no emails are sent). + */ + +package examples; + +import com.turbodocx.TurboDocxClient; +import com.turbodocx.models.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; + +public class TurboSignConditionalFields { + public static void main(String[] args) { + try { + TurboDocxClient client = new TurboDocxClient.Builder() + .apiKey(getEnv("TURBODOCX_API_KEY", "your-api-key-here")) + .orgId(getEnv("TURBODOCX_ORG_ID", "your-org-id-here")) + .senderEmail(getEnv("TURBODOCX_SENDER_EMAIL", "support@yourcompany.com")) + .senderName(getEnv("TURBODOCX_SENDER_NAME", "Your Company Name")) + .build(); + + byte[] pdfFile = Files.readAllBytes(Paths.get("../../ExampleAssets/advanced-contract.pdf")); + + System.out.println("Creating a review link with conditional fields..."); + + CreateSignatureReviewLinkResponse result = client.turboSign().createSignatureReviewLink( + new CreateSignatureReviewLinkRequest.Builder() + .file(pdfFile) + .fileName("advanced-contract.pdf") + .documentName("Conditional Fields Demo") + .recipients(Arrays.asList( + new Recipient("John Doe", "john@example.com", 1) + )) + .fields(Arrays.asList( + // Controlling checkboxes -- each carries a stable fieldKey. + new Field.Builder().type("checkbox").recipientEmail("john@example.com").page(1).x(60).y(120).width(20).height(20) + .metadata(FieldMetadata.forFieldKey("request_changes")).build(), + new Field.Builder().type("checkbox").recipientEmail("john@example.com").page(1).x(60).y(300).width(20).height(20) + .metadata(FieldMetadata.forFieldKey("override_amount")).build(), + new Field.Builder().type("checkbox").recipientEmail("john@example.com").page(1).x(60).y(480).width(20).height(20) + .metadata(FieldMetadata.forFieldKey("consent")).build(), + + // show + is_checked -- HIDDEN until "request_changes" is checked. + new Field.Builder().type("text").recipientEmail("john@example.com").page(1).x(120).y(120).width(260).height(40) + .metadata(FieldMetadata.forConditional(new FieldConditional("request_changes", "is_checked", "show"))).build(), + // ONE checkbox driving a SECOND dependent (same controllingFieldKey) -- a signature. + new Field.Builder().type("signature").recipientEmail("john@example.com").page(1).x(120).y(180).width(200).height(50) + .metadata(FieldMetadata.forConditional(new FieldConditional("request_changes", "is_checked", "show"))).build(), + + // unlock + is_checked -- VISIBLE but locked until "override_amount" is checked. + new Field.Builder().type("text").recipientEmail("john@example.com").page(1).x(120).y(300).width(150).height(30).defaultValue("1000.00") + .metadata(FieldMetadata.forConditional(new FieldConditional("override_amount", "is_checked", "unlock"))).build(), + + // show + is_not_checked -- a "please explain" box shown only while consent is WITHHELD. + new Field.Builder().type("text").recipientEmail("john@example.com").page(1).x(120).y(480).width(260).height(40) + .metadata(FieldMetadata.forConditional(new FieldConditional("consent", "is_not_checked", "show"))).build(), + + // A normal required signature with no rule -- always visible, always required. + new Field.Builder().type("signature").recipientEmail("john@example.com").page(1).x(120).y(620).width(200).height(50).required(true).build() + )) + .build() + ); + + System.out.println("✅ Review link created!"); + System.out.println("Document ID: " + result.getDocumentId()); + System.out.println("Preview URL: " + result.getPreviewUrl()); + + // Validation: a malformed rule (unknown operator/action, or a missing/empty + // controllingFieldKey) is rejected with HTTP 400 and code "InvalidConditionalRule". + // Fail-open: a well-formed rule whose controllingFieldKey matches NO checkbox is NOT an + // error -- the dependent field stays visible/editable. Double-check your keys match. + } catch (Exception error) { + System.out.println("Error: " + error.getMessage()); + } + } + + private static String getEnv(String key, String fallback) { + String value = System.getenv(key); + return (value != null && !value.isEmpty()) ? value : fallback; + } +} diff --git a/packages/js-sdk/examples/turbosign-conditional-fields.ts b/packages/js-sdk/examples/turbosign-conditional-fields.ts new file mode 100644 index 00000000..23f5d08a --- /dev/null +++ b/packages/js-sdk/examples/turbosign-conditional-fields.ts @@ -0,0 +1,117 @@ +/** + * Example: Conditional (IF/THEN) Fields + * + * A checkbox can control other fields so signers only see what applies to them: + * - Give a `checkbox` field a stable `metadata.fieldKey`. + * - Give a dependent field a `metadata.conditional` rule that references that key. + * - operator: "is_checked" | "is_not_checked" — when the rule fires. + * - action: "show" (hidden until the rule fires) + * "unlock" (visible but read-only until the rule fires). + * + * One checkbox can drive any number of dependent fields — give them the same + * `controllingFieldKey`. This example uses `createSignatureReviewLink` (no emails are + * sent) so you can run it and inspect the preview. + * + * Use this when: a form has follow-up questions that only matter in some cases + * ("If you request changes, explain what to change"). + */ + +import { TurboSign, ValidationError } from '@turbodocx/sdk'; +import * as fs from 'fs'; + +async function conditionalFieldsExample() { + TurboSign.configure({ + apiKey: process.env.TURBODOCX_API_KEY || 'your-api-key-here', + orgId: process.env.TURBODOCX_ORG_ID || 'your-org-id-here', + senderEmail: process.env.TURBODOCX_SENDER_EMAIL || 'support@yourcompany.com', + senderName: process.env.TURBODOCX_SENDER_NAME || 'Your Company Name' + }); + + try { + const pdfFile = fs.readFileSync('../../ExampleAssets/advanced-contract.pdf'); + + console.log('Creating a review link with conditional fields...\n'); + + const result = await TurboSign.createSignatureReviewLink({ + file: pdfFile, + documentName: 'Conditional Fields Demo', + recipients: [{ name: 'John Doe', email: 'john@example.com', signingOrder: 1 }], + fields: [ + // ── Controlling checkboxes — each carries a stable fieldKey ────────────── + { type: 'checkbox', recipientEmail: 'john@example.com', page: 1, x: 60, y: 120, width: 20, height: 20, metadata: { fieldKey: 'request_changes' } }, + { type: 'checkbox', recipientEmail: 'john@example.com', page: 1, x: 60, y: 300, width: 20, height: 20, metadata: { fieldKey: 'override_amount' } }, + { type: 'checkbox', recipientEmail: 'john@example.com', page: 1, x: 60, y: 480, width: 20, height: 20, metadata: { fieldKey: 'consent' } }, + + // show + is_checked — HIDDEN until "request_changes" is checked. + { + type: 'text', recipientEmail: 'john@example.com', page: 1, x: 120, y: 120, width: 260, height: 40, defaultValue: '', + metadata: { conditional: { controllingFieldKey: 'request_changes', operator: 'is_checked', action: 'show' } } + }, + // ONE checkbox driving a SECOND dependent (same controllingFieldKey) — a signature. + { + type: 'signature', recipientEmail: 'john@example.com', page: 1, x: 120, y: 180, width: 200, height: 50, + metadata: { conditional: { controllingFieldKey: 'request_changes', operator: 'is_checked', action: 'show' } } + }, + + // unlock + is_checked — VISIBLE but locked (read-only) until "override_amount" is checked. + { + type: 'text', recipientEmail: 'john@example.com', page: 1, x: 120, y: 300, width: 150, height: 30, defaultValue: '1000.00', + metadata: { conditional: { controllingFieldKey: 'override_amount', operator: 'is_checked', action: 'unlock' } } + }, + + // show + is_not_checked — a "please explain" box shown only while consent is WITHHELD. + { + type: 'text', recipientEmail: 'john@example.com', page: 1, x: 120, y: 480, width: 260, height: 40, defaultValue: '', + metadata: { conditional: { controllingFieldKey: 'consent', operator: 'is_not_checked', action: 'show' } } + }, + + // A normal required signature with no rule — always visible, always required. + { type: 'signature', recipientEmail: 'john@example.com', page: 1, x: 120, y: 620, width: 200, height: 50, required: true } + ] + }); + + console.log('✅ Review link created!\n'); + console.log('Document ID:', result.documentId); + console.log('Preview URL:', result.previewUrl); + + // ── Good to know ───────────────────────────────────────────────────────────── + // Validation: a malformed rule (unknown operator/action, or a missing/empty + // controllingFieldKey) is rejected with HTTP 400 and code "InvalidConditionalRule". + // You can catch it as a ValidationError — see conditionalValidationExample() below. + // Fail-open: a well-formed rule whose controllingFieldKey matches NO checkbox is NOT an + // error — the dependent field simply stays visible/editable (so a typo can't strand a + // field as permanently hidden). Double-check your keys match exactly. + } catch (error) { + console.error('Error:', error); + } +} + +/** + * Optional: shows how a malformed rule surfaces. The API rejects it BEFORE creating anything. + */ +async function conditionalValidationExample() { + try { + await TurboSign.createSignatureReviewLink({ + file: fs.readFileSync('../../ExampleAssets/advanced-contract.pdf'), + recipients: [{ name: 'John Doe', email: 'john@example.com', signingOrder: 1 }], + fields: [ + { type: 'checkbox', recipientEmail: 'john@example.com', page: 1, x: 60, y: 120, width: 20, height: 20, metadata: { fieldKey: 'agree' } }, + { + type: 'text', recipientEmail: 'john@example.com', page: 1, x: 120, y: 120, width: 260, height: 40, + // "is_ticked" is not a valid operator — the API will reject this. + metadata: { conditional: { controllingFieldKey: 'agree', operator: 'is_ticked' as never, action: 'show' } } + } + ] + }); + } catch (error) { + if (error instanceof ValidationError) { + console.log(`Rejected as expected: ${error.code} — ${error.message}`); + } else { + throw error; + } + } +} + +// Run the example +conditionalFieldsExample(); +// conditionalValidationExample(); diff --git a/packages/php-sdk/examples/turbosign-conditional-fields.php b/packages/php-sdk/examples/turbosign-conditional-fields.php new file mode 100644 index 00000000..28bc919a --- /dev/null +++ b/packages/php-sdk/examples/turbosign-conditional-fields.php @@ -0,0 +1,102 @@ +documentId}\n"; + echo "Preview URL: {$result->previewUrl}\n"; + + // Validation: a malformed rule (unknown operator/action, or a missing/empty + // controllingFieldKey) is rejected with HTTP 400 and code "InvalidConditionalRule" + // (thrown as a ValidationError). + // Fail-open: a well-formed rule whose controllingFieldKey matches NO checkbox is NOT an + // error -- the dependent field stays visible/editable. Double-check your keys match. + } catch (\Throwable $error) { + echo "Error: {$error->getMessage()}\n"; + } +} + +conditionalFieldsExample(); diff --git a/packages/py-sdk/examples/turbosign_conditional_fields.py b/packages/py-sdk/examples/turbosign_conditional_fields.py new file mode 100644 index 00000000..7909d527 --- /dev/null +++ b/packages/py-sdk/examples/turbosign_conditional_fields.py @@ -0,0 +1,82 @@ +""" +Example: Conditional (IF/THEN) Fields + +A checkbox can control other fields so signers only see what applies to them: + - Give a "checkbox" field a stable metadata.fieldKey. + - Give a dependent field a metadata.conditional rule that references that key. + operator: "is_checked" | "is_not_checked" -- when the rule fires. + action: "show" (hidden until the rule fires) + "unlock" (visible but read-only until the rule fires). + +One checkbox can drive any number of dependent fields -- give them the same +controllingFieldKey. Uses create_signature_review_link (no emails are sent) so you can +run it and inspect the preview. + +Use this when: a form has follow-up questions that only matter in some cases +("If you request changes, explain what to change"). +""" + +import asyncio +import os +from turbodocx_sdk import TurboSign + + +async def conditional_fields_example(): + TurboSign.configure( + api_key=os.getenv("TURBODOCX_API_KEY", "your-api-key-here"), + org_id=os.getenv("TURBODOCX_ORG_ID", "your-org-id-here"), + sender_email=os.getenv("TURBODOCX_SENDER_EMAIL", "support@yourcompany.com"), + sender_name=os.getenv("TURBODOCX_SENDER_NAME", "Your Company Name"), + ) + + try: + with open("../../ExampleAssets/advanced-contract.pdf", "rb") as f: + pdf_file = f.read() + + print("Creating a review link with conditional fields...\n") + + # NOTE: keys inside a field dict stay camelCase -- they are sent to the API verbatim. + result = await TurboSign.create_signature_review_link( + file=pdf_file, + document_name="Conditional Fields Demo", + recipients=[{"name": "John Doe", "email": "john@example.com", "signingOrder": 1}], + fields=[ + # Controlling checkboxes -- each carries a stable fieldKey. + {"type": "checkbox", "recipientEmail": "john@example.com", "page": 1, "x": 60, "y": 120, "width": 20, "height": 20, "metadata": {"fieldKey": "request_changes"}}, + {"type": "checkbox", "recipientEmail": "john@example.com", "page": 1, "x": 60, "y": 300, "width": 20, "height": 20, "metadata": {"fieldKey": "override_amount"}}, + {"type": "checkbox", "recipientEmail": "john@example.com", "page": 1, "x": 60, "y": 480, "width": 20, "height": 20, "metadata": {"fieldKey": "consent"}}, + + # show + is_checked -- HIDDEN until "request_changes" is checked. + {"type": "text", "recipientEmail": "john@example.com", "page": 1, "x": 120, "y": 120, "width": 260, "height": 40, "defaultValue": "", + "metadata": {"conditional": {"controllingFieldKey": "request_changes", "operator": "is_checked", "action": "show"}}}, + # ONE checkbox driving a SECOND dependent (same controllingFieldKey) -- a signature. + {"type": "signature", "recipientEmail": "john@example.com", "page": 1, "x": 120, "y": 180, "width": 200, "height": 50, + "metadata": {"conditional": {"controllingFieldKey": "request_changes", "operator": "is_checked", "action": "show"}}}, + + # unlock + is_checked -- VISIBLE but locked until "override_amount" is checked. + {"type": "text", "recipientEmail": "john@example.com", "page": 1, "x": 120, "y": 300, "width": 150, "height": 30, "defaultValue": "1000.00", + "metadata": {"conditional": {"controllingFieldKey": "override_amount", "operator": "is_checked", "action": "unlock"}}}, + + # show + is_not_checked -- a "please explain" box shown only while consent is WITHHELD. + {"type": "text", "recipientEmail": "john@example.com", "page": 1, "x": 120, "y": 480, "width": 260, "height": 40, "defaultValue": "", + "metadata": {"conditional": {"controllingFieldKey": "consent", "operator": "is_not_checked", "action": "show"}}}, + + # A normal required signature with no rule -- always visible, always required. + {"type": "signature", "recipientEmail": "john@example.com", "page": 1, "x": 120, "y": 620, "width": 200, "height": 50, "required": True}, + ], + ) + + print("✅ Review link created!\n") + print("Document ID:", result.get("documentId")) + print("Preview URL:", result.get("previewUrl")) + + # Validation: a malformed rule (unknown operator/action, or a missing/empty + # controllingFieldKey) is rejected with HTTP 400 and code "InvalidConditionalRule" + # (raised as a ValidationError). + # Fail-open: a well-formed rule whose controllingFieldKey matches NO checkbox is NOT an + # error -- the dependent field simply stays visible/editable. Double-check your keys. + except Exception as error: + print("Error:", error) + + +asyncio.run(conditional_fields_example()) diff --git a/packages/ruby-sdk/examples/turbosign_conditional_fields.rb b/packages/ruby-sdk/examples/turbosign_conditional_fields.rb new file mode 100644 index 00000000..0536e86e --- /dev/null +++ b/packages/ruby-sdk/examples/turbosign_conditional_fields.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# Example: Conditional (IF/THEN) Fields +# +# A checkbox can control other fields so signers only see what applies to them: +# - Give a "checkbox" field a stable metadata.fieldKey. +# - Give a dependent field a metadata.conditional rule that references that key. +# operator: "is_checked" | "is_not_checked" -- when the rule fires. +# action: "show" (hidden until the rule fires) +# "unlock" (visible but read-only until the rule fires). +# +# One checkbox can drive any number of dependent fields -- give them the same +# controllingFieldKey. Uses create_signature_review_link (no emails are sent). +# +# Set environment variables before running: +# export TURBODOCX_API_KEY=your-api-key +# export TURBODOCX_ORG_ID=your-org-uuid +# export TURBODOCX_SENDER_EMAIL=support@yourcompany.com +# export TURBODOCX_SENDER_NAME="Your Company Name" + +require "stringio" +require "turbodocx_sdk" + +TurboDocxSdk::TurboSign.configure( + api_key: ENV.fetch("TURBODOCX_API_KEY", "your-api-key-here"), + org_id: ENV.fetch("TURBODOCX_ORG_ID", "your-org-id-here"), + sender_email: ENV.fetch("TURBODOCX_SENDER_EMAIL", "support@yourcompany.com"), + sender_name: ENV.fetch("TURBODOCX_SENDER_NAME", "Your Company Name") +) + +begin + pdf_file = StringIO.new(File.binread("../../ExampleAssets/advanced-contract.pdf")) + + puts "Creating a review link with conditional fields...\n\n" + + # NOTE: keys inside a field hash stay camelCase -- they are sent to the API verbatim. + result = TurboDocxSdk::TurboSign.create_signature_review_link( + "file" => pdf_file, + "documentName" => "Conditional Fields Demo", + "recipients" => [{ "name" => "John Doe", "email" => "john@example.com", "signingOrder" => 1 }], + "fields" => [ + # Controlling checkboxes -- each carries a stable fieldKey. + { "type" => "checkbox", "recipientEmail" => "john@example.com", "page" => 1, "x" => 60, "y" => 120, "width" => 20, "height" => 20, "metadata" => { "fieldKey" => "request_changes" } }, + { "type" => "checkbox", "recipientEmail" => "john@example.com", "page" => 1, "x" => 60, "y" => 300, "width" => 20, "height" => 20, "metadata" => { "fieldKey" => "override_amount" } }, + { "type" => "checkbox", "recipientEmail" => "john@example.com", "page" => 1, "x" => 60, "y" => 480, "width" => 20, "height" => 20, "metadata" => { "fieldKey" => "consent" } }, + + # show + is_checked -- HIDDEN until "request_changes" is checked. + { "type" => "text", "recipientEmail" => "john@example.com", "page" => 1, "x" => 120, "y" => 120, "width" => 260, "height" => 40, "defaultValue" => "", + "metadata" => { "conditional" => { "controllingFieldKey" => "request_changes", "operator" => "is_checked", "action" => "show" } } }, + # ONE checkbox driving a SECOND dependent (same controllingFieldKey) -- a signature. + { "type" => "signature", "recipientEmail" => "john@example.com", "page" => 1, "x" => 120, "y" => 180, "width" => 200, "height" => 50, + "metadata" => { "conditional" => { "controllingFieldKey" => "request_changes", "operator" => "is_checked", "action" => "show" } } }, + + # unlock + is_checked -- VISIBLE but locked until "override_amount" is checked. + { "type" => "text", "recipientEmail" => "john@example.com", "page" => 1, "x" => 120, "y" => 300, "width" => 150, "height" => 30, "defaultValue" => "1000.00", + "metadata" => { "conditional" => { "controllingFieldKey" => "override_amount", "operator" => "is_checked", "action" => "unlock" } } }, + + # show + is_not_checked -- a "please explain" box shown only while consent is WITHHELD. + { "type" => "text", "recipientEmail" => "john@example.com", "page" => 1, "x" => 120, "y" => 480, "width" => 260, "height" => 40, "defaultValue" => "", + "metadata" => { "conditional" => { "controllingFieldKey" => "consent", "operator" => "is_not_checked", "action" => "show" } } }, + + # A normal required signature with no rule -- always visible, always required. + { "type" => "signature", "recipientEmail" => "john@example.com", "page" => 1, "x" => 120, "y" => 620, "width" => 200, "height" => 50, "required" => true } + ] + ) + + puts "✅ Review link created!\n\n" + puts "Document ID: #{result['documentId']}" + puts "Preview URL: #{result['previewUrl']}" + + # Validation: a malformed rule (unknown operator/action, or a missing/empty controllingFieldKey) + # is rejected with HTTP 400 and code "InvalidConditionalRule" (raised as a ValidationError). + # Fail-open: a well-formed rule whose controllingFieldKey matches NO checkbox is NOT an error -- + # the dependent field simply stays visible/editable. Double-check your keys match exactly. +rescue StandardError => e + puts "Error: #{e.message}" +end From 89cc1dcc2bbed1cb86807568c6e32f4b67ce6743 Mon Sep 17 00:00:00 2001 From: Amit Sharma Date: Wed, 19 Aug 2026 21:04:25 +0530 Subject: [PATCH 3/3] style(examples): satisfy php-cs-fixer in the conditional-fields example php-cs-fixer's ensure_fully_multiline requires one named argument per line on a multi-line call. The four dependent-field new Field(...) constructors had their args wrapped two-to-a-line, which failed the cs-fix --dry-run check. Reformat them one-arg-per-line to match; the single-line checkbox constructors are unaffected. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../examples/turbosign-conditional-fields.php | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/php-sdk/examples/turbosign-conditional-fields.php b/packages/php-sdk/examples/turbosign-conditional-fields.php index 28bc919a..e8f632b7 100644 --- a/packages/php-sdk/examples/turbosign-conditional-fields.php +++ b/packages/php-sdk/examples/turbosign-conditional-fields.php @@ -56,24 +56,49 @@ function conditionalFieldsExample(): void // show + is_checked -- HIDDEN until "request_changes" is checked. new Field( - type: SignatureFieldType::TEXT, recipientEmail: 'john@example.com', page: 1, x: 120, y: 120, width: 260, height: 40, + type: SignatureFieldType::TEXT, + recipientEmail: 'john@example.com', + page: 1, + x: 120, + y: 120, + width: 260, + height: 40, metadata: new FieldMetadata(conditional: new FieldConditional(controllingFieldKey: 'request_changes', operator: ConditionalOperator::IS_CHECKED, action: ConditionalAction::SHOW)) ), // ONE checkbox driving a SECOND dependent (same controllingFieldKey) -- a signature. new Field( - type: SignatureFieldType::SIGNATURE, recipientEmail: 'john@example.com', page: 1, x: 120, y: 180, width: 200, height: 50, + type: SignatureFieldType::SIGNATURE, + recipientEmail: 'john@example.com', + page: 1, + x: 120, + y: 180, + width: 200, + height: 50, metadata: new FieldMetadata(conditional: new FieldConditional(controllingFieldKey: 'request_changes', operator: ConditionalOperator::IS_CHECKED, action: ConditionalAction::SHOW)) ), // unlock + is_checked -- VISIBLE but locked until "override_amount" is checked. new Field( - type: SignatureFieldType::TEXT, recipientEmail: 'john@example.com', page: 1, x: 120, y: 300, width: 150, height: 30, defaultValue: '1000.00', + type: SignatureFieldType::TEXT, + recipientEmail: 'john@example.com', + page: 1, + x: 120, + y: 300, + width: 150, + height: 30, + defaultValue: '1000.00', metadata: new FieldMetadata(conditional: new FieldConditional(controllingFieldKey: 'override_amount', operator: ConditionalOperator::IS_CHECKED, action: ConditionalAction::UNLOCK)) ), // show + is_not_checked -- a "please explain" box shown only while consent is WITHHELD. new Field( - type: SignatureFieldType::TEXT, recipientEmail: 'john@example.com', page: 1, x: 120, y: 480, width: 260, height: 40, + type: SignatureFieldType::TEXT, + recipientEmail: 'john@example.com', + page: 1, + x: 120, + y: 480, + width: 260, + height: 40, metadata: new FieldMetadata(conditional: new FieldConditional(controllingFieldKey: 'consent', operator: ConditionalOperator::IS_NOT_CHECKED, action: ConditionalAction::SHOW)) ),