diff --git a/LICENSE b/LICENSE index 824a3b8..59cfbe8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2024 airSlate, Inc. (https://www.signnow.com) +Copyright (c) 2003-present SignNow Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index daa0e63..00880dc 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ setup-env: @echo "\nPlease configure your sample application with required information:"; \ read -p "signNow API host: " sn_api_host; \ read -p "signNow application's basic token: " sn_basic_token; \ - read -p "Your signer's email: " sn_signer_emal; \ + read -p "Your signer's email: " sn_signer_email; \ read -p "Your login to signNow account: " sn_user; \ read -p "Your password to signNow account: " sn_password; \ echo "\n" >> .env; \ @@ -34,6 +34,6 @@ setup-env: echo "SIGNNOW_API_BASIC_TOKEN=$$sn_basic_token" >> .env; \ echo "SIGNNOW_API_USERNAME=$$sn_user" >> .env; \ echo "SIGNNOW_API_PASSWORD=$$sn_password" >> .env; \ - echo "SIGNNOW_SIGNER_EMAIL=$$sn_signer_emal" >> .env; \ + echo "SIGNNOW_SIGNER_EMAIL=$$sn_signer_email" >> .env; \ echo "\n" >> .env; \ echo "Setup completed." diff --git a/README.md b/README.md index d2e3bef..1ede1f4 100644 --- a/README.md +++ b/README.md @@ -1,150 +1,189 @@ -# signNow Sample App +# SignNow PHP Sample App -[![PHP Version](https://img.shields.io/badge/php->=8.2-blue)](https://php.net/) -[![Laravel Version](https://img.shields.io/badge/laravel-10-cyan)](https://laravel.com/) -[![signNow PHP SDK](https://img.shields.io/badge/signNow_SDK-2.2-light)](https://github.com/signnow/SignNowPHPSDK) -[![Licence](https://img.shields.io/badge/license-MIT-green)](./LICENSE) -## About - -This sample application demonstrates how the signNow API and PHP SDK can be used to construct applications with signing flows. This sample application operates using the following features: - -* [Authentication](https://docs.signnow.com/docs/signnow/j6jxdlizr86se-generate-access-token) -* [Upload document](https://docs.signnow.com/docs/signnow/i4216w3e1jv3p-upload-document) -* [Add fillable fields](https://docs.signnow.com/docs/signnow/xsttpdx7r60iw-edit-document) -* [Embedded signing](https://docs.signnow.com/docs/signnow/document-embedded-signing/operations/create-a-v-2-document-embedded-invite) - -Try using the sample application for testing purposes or use it as a skeleton for your own application. To create an application that generates legally binding signatures, you’ll need a signNow subscription. - -## Prerequisites - -* Create a signNow account: - * For integration purposes, you need a signNow account with a paid subscription. - * For testing purposes, you can [create](https://www.signnow.com/developers) a signNow developer account. -* At the [API Dashboard](https://app.signnow.com/webapp/api-dashboard/keys), create an application. -* Use your signNow credentials and basic authorization token in your config file `.env`. - -## Environment variables - -The configuration file is located in the project directory with the common name `.env`. - -Configure the following variables to ensure the application’s proper functionality. - -| Variable | Example | Description | -|--------------------|--------------------------------------------|------------------------------------------------------------------------------------------------| -| SIGNNOW_API_HOST | `https://api.signnow.com` | signNow API host | -| SIGNNOW_API_BASIC_TOKEN | `c2lnbk5vdyBBUEkgc2FtcGxlIEFwcCB2MS4wCg==` | Find your basic token at the [API Dashboard](https://app.signnow.com/webapp/api-dashboard/keys). | -| SIGNNOW_API_USERNAME | `user@mailer.com` | The email address of the document signer. | -| SIGNNOW_API_PASSWORD | `*****` | Your signNow account password. | -| SIGNNOW_SIGNER_EMAIL | `signer@mailer.com` | The email address of the person who is supposed to sign a document. | - -View the entire configuration file, including standard Laravel variables, [here](./.env.example). - -## Get Started -1. Clone the repository - ``` - git clone git@github.com:signnow/sample-app.git - ``` - -2. Build a docker image using the following command: - - ``` - make build - ``` - -3. Start the `signnow-sample-app` docker container using the following command: +[![PHP](https://img.shields.io/badge/php-8.2-blue)](https://www.php.net/) +[![Laravel](https://img.shields.io/badge/laravel-10-cyan)](https://laravel.com/) +[![SignNow SDK](https://img.shields.io/badge/SignNow_SDK-3.x-light)](https://github.com/signnow/SignNowPHPSDK) +[![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE) +A Laravel 10 application demonstrating the SignNow API via the official [`signnow/api-php-sdk`](https://github.com/signnow/SignNowPHPSDK) package. + +## Quick Start + +### 1. Enter the directory + +```bash +cd SampleApps/php-sample-app +``` + +### 2. Configure environment + +```bash +cp .env.example .env +``` + +Edit `.env` and fill in your SignNow credentials: + +| Variable | Example | Description | +|---|---|---| +| `SN_API_HOST` | `https://api.signnow.com` | Production or sandbox URL | +| `SN_API_BASIC_TOKEN` | `c2lnbk5vdy4...` | Base64 token from [API Dashboard](https://app.signnow.com/webapp/api-dashboard/keys) | +| `SN_API_USER` | `you@example.com` | Your SignNow account email | +| `SN_API_PASSWORD` | `••••••` | Your SignNow account password | +| `SN_SIGNER_EMAIL` | `signer@example.com` | Default embedded signer email | + +### 3. Run with Docker (recommended) + +```bash +make build # build Docker image +make setup # install dependencies + generate Laravel key +make up # start the container +``` + +App is available at `http://localhost:8080`. Stop with `make down`. + +Other useful targets: + +```bash +make clear # clear Laravel caches +make setup-env # interactive credential prompt +``` + +### 4. Run with Docker (without make) + +```bash +docker compose build +docker compose up -d +docker compose exec app composer install +docker compose exec app php artisan key:generate +``` + +### 5. Run locally (no Docker) + +```bash +composer install +php artisan key:generate +php artisan serve --port=8080 +``` + +Requires PHP 8.2+ with the `curl`, `mbstring`, `openssl`, and `xml` extensions. + +### 6. Open a sample + +``` +http://localhost:8080/samples/EmbeddedSignerConsentForm +``` + +`http://localhost:8080/samples` lists all available samples. + +## Available Samples (20) + +| Sample | Description | +|---|---| +| EmbeddedSignerConsentForm | Consent form with single embedded signer | +| EmbeddedSenderWithoutFormFile | Sales proposal — embedded sender | +| EmbeddedSenderWithFormCreditLoanAgreement | Credit loan agreement | +| EmbeddedSignerConsumerServices | Veterinary intake form | +| EmbeddedSignerPatientIntakeForm | Patient intake (healthcare) | +| EmbeddedSignerWithFormInsurance | Insurance claim form | +| MedicalInsuranceClaimForm | Medical insurance claim | +| EmbeddedEditingAndSigningDG | Document generation: edit + sign | +| EmbeddedSenderWithFormAndFirstSigner | Sender is also first signer | +| EmbeddedSenderWithFormDG | Document generation variant | +| EmbeddedSenderWithFormDGAdjunct | DG adjunct form | +| EmbeddedSenderWithFormDGConstr | DG construction form | +| ISVWithFormAndOneClickSendBasicPrefill | ISV one-click send, basic prefill | +| ISVWithFormAndOneClickSendMergeFields | ISV one-click send, merge fields | +| HROnboardingSystem | HR onboarding multi-document flow | +| UploadEmbeddedSender | Upload PDF + embedded sender | +| PrefillAndEmbeddedSendingAgreement | Prefill + embedded send | +| PrefillAndOneClickSendingAgreement | Prefill + one-click send | +| EVDemoSendingAnd3EmbeddedSigners | Real estate: 3 sequential embedded signers | +| UploadEmbeddedEditingAndInvite | Upload PDF, embedded edit, invite | + +## Project Structure + +``` +app/Http/Controllers/ + RoutingController.php GET /samples/:name, POST /api/samples/:name + SampleControllerInterface.php Sample contract (handleGet + handlePost) + ControllerFactory.php Loads sample controllers by name +app/Services/ + SamplesProvider.php Validates names against whitelist +routes/ + web.php Route definitions +samples/ + / + SampleController.php Implements SampleControllerInterface + Meta.json Sample metadata (title, description) + index.blade.php Blade template served on GET /samples/ +resources/views/ Shared Blade layouts +static/ Shared CSS, JS, images +samples_witelist.json Authorised sample names +``` + +## Routing + +| Method | Path | Handler | +|---|---|---| +| GET | `/samples/{name}` | `samples//SampleController.handleGet` | +| POST | `/api/samples/{name}` | `samples//SampleController.handlePost` | +| GET | `/css/*`, `/img/*`, etc. | Shared static files | + +Sample names must match `^[a-zA-Z0-9_]+$` and must be listed in `samples_witelist.json`. Unlisted names → 404. + +## Add a New Sample + +1. `mkdir samples/MyNewSample` +2. Add `"MyNewSample"` to `samples_witelist.json`. +3. Create `samples/MyNewSample/Meta.json`: + ```json + { "title": "My New Sample", "description": "What it demonstrates." } ``` - make up +4. Create `samples/MyNewSample/SampleController.php`: + ```php + ` tag. Using this method, the signing session is built into the website. You can also customize the signing experience by using the [branding feature](https://docs.signnow.com/docs/signnow/branches/v1.2/guides-branding). All backend operations are performed using the signNow PHP SDK. - -## More information - -| Topic | URL | -|-----------------------|--------------------------------------------| -| signNow website | | -| PHP SDK | | -| signNow documentation | | - -____ - -## Ready to build eSignature integrations with SignNow API? Get the SignNow extension for GitHub Copilot - -Use AI-powered code suggestions to generate SignNow API code snippets in your IDE with GitHub Copilot. Get examples for common integration tasks—from authentication and sending documents for signature to handling webhooks, and building branded workflows. - -### **🚀 Why use SignNow with GitHub Copilot** - -* **Relevant code suggestions**: Get AI-powered, up-to-date code snippets for SignNow API calls. All examples reflect the latest API capabilities and follow current best practices. -* **Faster development**: Reduce time spent searching documentation. -* **Fewer mistakes**: Get context-aware guidance aligned with the SignNow API. -* **Smooth onboarding**: Useful for both new and experienced developers working with the API. +## Tests -### **Prerequisites:** +No automated tests are included. The application can be verified manually by running each sample end-to-end. -1\. GitHub Copilot installed and enabled. -2\. SignNow account. [Register here](https://www.signnow.com/developers) +## SDK Notes -### ⚙️ **How to use it** +Known quirks in `signnow/api-php-sdk` 3.x that affect this codebase: -1\. Install the [SignNow extension](https://github.com/apps/signnow). +**`To` constructor parameter order** — `new To(email, roleId, role, order, subject, message)`. Note that `roleId` (the `unique_id` from the roles array) and `role` (the role name string) are separate parameters in positions 2 and 3. -2\. Start your prompts with [@signnow](https://github.com/signnow) in the Copilot chat window. The first time you use the extension, you may need to authorize it. +**`SendInvitePost` constructor** — `new SendInvitePost(documentId, ToCollection, from, subject, message)`. The sender email (`from`) must be the authenticated account's email. -3\. Enter a prompt describing the integration scenario. -Example: @signnow Generate a Java code example for sending a document group to two signers. +**Embedded sending vs embedded signing mode** — `DocumentEmbeddedSendingLinkPost` takes a `mode` parameter: `'document'` opens the embedded sender UI; `'invite'` opens the embedded signer UI. Passing the wrong mode is a silent failure — the link works but shows the wrong interface. -4\. Modify the generated code to match your app’s requirements—adjust parameters, headers, and workflows as needed. +**`DocumentGet` response roles** — `getRoles()` returns an array where each role has `getName()` (display name) and `getUniqueId()` (the ID to pass as `roleId` in invites). Do not confuse the two. -### **Troubleshooting** -**The extension doesn’t provide code examples for the SignNow API** +## Tech Stack -Make sure you're using `@signnow` in the Copilot chat and that the extension is installed and authorized. -____ +- PHP 8.2 +- Laravel 10 +- `signnow/api-php-sdk` 3.x (from Packagist) +- Guzzle 7.9 (HTTP client, pulled in by SDK) +- Docker: `php:8.2-fpm` + nginx -# Explore SignNow developer tools -## [**API reference**](https://docs.signnow.com/docs/signnow/reference) -Get technical details about SignNow API requests, parameters, code examples, and possible errors. Send API calls directly from the documentation using the **Try it** console. -## [**SDKs**](https://docs.signnow.com/docs/signnow/sdk) -Use official SignNow API libraries and sample code to integrate advanced eSignature functionality into your software. -## [**GitHub Copilot extension**](https://github.com/apps/signnow) -Develop eSignature integrations directly in GitHub using AI-powered code suggestions. Copilot recommends API calls and code snippets that align with SignNow API guidelines. -## [Postman collection](https://www.postman.com/signnow-api/signnow-public-collection/overview) -Explore and test SignNow API requests in Postman. Use pre-configured collections to quickly start building and debugging your integrations. +## GitHub Copilot Extension -____ +Get AI-powered SignNow code suggestions in your IDE: +[github.com/apps/signnow](https://github.com/apps/signnow) — start prompts with `@signnow`. ## License -This repository is under the MIT license. You are free to use the code of this application as a skeleton for your application. For more information, see [LICENSE](./LICENSE). +See [LICENSE](./LICENSE). diff --git a/samples/EmbeddedSenderWithFormAndFirstSigner/SampleController.php b/samples/EmbeddedSenderWithFormAndFirstSigner/SampleController.php index 67d4d9f..1819cb6 100644 --- a/samples/EmbeddedSenderWithFormAndFirstSigner/SampleController.php +++ b/samples/EmbeddedSenderWithFormAndFirstSigner/SampleController.php @@ -30,6 +30,7 @@ use SignNow\Api\DocumentGroup\Request\Data\Recipient\DocumentCollection as RecipientDocumentCollection; use SignNow\ApiClient; +use SignNow\Exception\SignNowApiException; use SignNow\Sdk; use Symfony\Component\HttpFoundation\Response; use SplFileInfo; @@ -60,10 +61,14 @@ public function handlePost(Request $request): Response { $action = $request->input('action'); - $sdk = new Sdk(); - $apiClient = $sdk->build() - ->authenticate() - ->getApiClient(); + try { + $sdk = new Sdk(); + $apiClient = $sdk->build() + ->authenticate() + ->getApiClient(); + } catch (SignNowApiException $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } switch ($action) { case 'prepare_dg': @@ -88,37 +93,41 @@ private function prepareDocumentGroup(Request $request, ApiClient $apiClient): J return response()->json(['success' => false, 'message' => 'Name and email are required'], 400); } - // 1. Create Document Group from Template - $dgResponse = $this->createDocumentGroupFromTemplate($apiClient); - if (!$dgResponse['success']) { - return response()->json($dgResponse, 500); - } + try { + // 1. Create Document Group from Template + $dgResponse = $this->createDocumentGroupFromTemplate($apiClient); + if (!$dgResponse['success']) { + return response()->json($dgResponse, 500); + } - $documentGroupId = $dgResponse['document_group_id']; + $documentGroupId = $dgResponse['document_group_id']; - // 2. Update document fields with names - $updateFieldsResponse = $this->updateDocumentFields($apiClient, $documentGroupId, $name); - if (!$updateFieldsResponse['success']) { - return response()->json($updateFieldsResponse, 500); - } + // 2. Update document fields with names + $updateFieldsResponse = $this->updateDocumentFields($apiClient, $documentGroupId, $name); + if (!$updateFieldsResponse['success']) { + return response()->json($updateFieldsResponse, 500); + } - // 3. Add recipients to Document Group with different emails for different roles - $customerEmail = $email; // Email from form for "Customer to Sign" - $preparerEmail = config('signnow.api.user'); // Email from config for "Prepare Contract" - $addRecipientsResponse = $this->updateDocumentGroupRecipients( - $apiClient, - $documentGroupId, - $customerEmail, - $preparerEmail - ); - if (!$addRecipientsResponse['success']) { - return response()->json($addRecipientsResponse, 500); - } + // 3. Add recipients to Document Group with different emails for different roles + $customerEmail = $email; // Email from form for "Customer to Sign" + $preparerEmail = config('signnow.api.user'); // Email from config for "Prepare Contract" + $addRecipientsResponse = $this->updateDocumentGroupRecipients( + $apiClient, + $documentGroupId, + $customerEmail, + $preparerEmail + ); + if (!$addRecipientsResponse['success']) { + return response()->json($addRecipientsResponse, 500); + } - // 4. Create embedded sending link - $embeddedSendingResponse = $this->createEmbeddedSendingUrl($apiClient, $documentGroupId); - if (!$embeddedSendingResponse['success']) { - return response()->json($embeddedSendingResponse, 500); + // 4. Create embedded sending link + $embeddedSendingResponse = $this->createEmbeddedSendingUrl($apiClient, $documentGroupId); + if (!$embeddedSendingResponse['success']) { + return response()->json($embeddedSendingResponse, 500); + } + } catch (SignNowApiException $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } return response()->json([ @@ -132,10 +141,14 @@ private function createSigningUrl(Request $request, ApiClient $apiClient): JsonR { $documentGroupId = $request->input('document_group_id'); - // Create signing link with limited token - $signingLinkResponse = $this->createSigningLink($apiClient, $documentGroupId); - if (!$signingLinkResponse['success']) { - return response()->json($signingLinkResponse, 500); + try { + // Create signing link with limited token + $signingLinkResponse = $this->createSigningLink($apiClient, $documentGroupId); + if (!$signingLinkResponse['success']) { + return response()->json($signingLinkResponse, 500); + } + } catch (SignNowApiException $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } return response()->json([ @@ -149,7 +162,11 @@ private function createSigningUrl(Request $request, ApiClient $apiClient): JsonR private function getInviteStatus(Request $request, ApiClient $apiClient): JsonResponse { $documentGroupId = $request->input('document_group_id'); - $signers = $this->getDocumentGroupSignersStatus($apiClient, $documentGroupId); + try { + $signers = $this->getDocumentGroupSignersStatus($apiClient, $documentGroupId); + } catch (SignNowApiException $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } return response()->json($signers); } @@ -157,7 +174,11 @@ private function downloadDocumentGroup(Request $request, ApiClient $apiClient): { $documentGroupId = $request->input('document_group_id'); - $file = $this->downloadDocumentGroupFile($apiClient, $documentGroupId); + try { + $file = $this->downloadDocumentGroupFile($apiClient, $documentGroupId); + } catch (SignNowApiException $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } $content = file_get_contents($file->getRealPath()); unlink($file->getRealPath()); diff --git a/samples/EmbeddedSignerPatientIntakeForm/SampleController.php b/samples/EmbeddedSignerPatientIntakeForm/SampleController.php index a6353b3..95c172f 100644 --- a/samples/EmbeddedSignerPatientIntakeForm/SampleController.php +++ b/samples/EmbeddedSignerPatientIntakeForm/SampleController.php @@ -20,6 +20,7 @@ use SignNow\Api\EmbeddedInvite\Response\DocumentInviteLinkPost as DocumentInviteLinkPostResponse; use SignNow\Api\EmbeddedInvite\Response\DocumentInvitePost as DocumentInvitePostReponse; use SignNow\ApiClient; +use SignNow\Exception\SignNowApiException; use SignNow\Sdk; use SignNow\Api\Template\Response\CloneTemplatePost as CloneTemplatePostResponse; use SignNow\Api\Document\Response\DocumentDownloadGet as DocumentDownloadGetResponse; @@ -49,7 +50,11 @@ public function handleGet(Request $request): Response ] ); } else { - $link = $this->createEmbeddedInviteAndReturnSigningLink(self::TEMPLATE_ID); + try { + $link = $this->createEmbeddedInviteAndReturnSigningLink(self::TEMPLATE_ID); + } catch (SignNowApiException $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } return new RedirectResponse($link); } @@ -65,12 +70,16 @@ public function handlePost(Request $request): Response { $request->get('document_id'); - $sdk = new Sdk(); - $apiClient = $sdk->build() - ->authenticate() - ->getApiClient(); + try { + $sdk = new Sdk(); + $apiClient = $sdk->build() + ->authenticate() + ->getApiClient(); - $file = $this->downloadDocument($apiClient, $request->get('document_id')); + $file = $this->downloadDocument($apiClient, $request->get('document_id')); + } catch (SignNowApiException $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } return new Response($file, 200, [ 'Content-Type' => 'application/pdf', diff --git a/samples/PrefillAndEmbeddedSendingAgreement/ReadMe.md b/samples/PrefillAndEmbeddedSendingAgreement/ReadMe.md new file mode 100644 index 0000000..8735915 --- /dev/null +++ b/samples/PrefillAndEmbeddedSendingAgreement/ReadMe.md @@ -0,0 +1,72 @@ +# Prefill and Embedded Sending Agreement + +## Use Case Overview + +This sample demonstrates how to use the SignNow PHP SDK to implement a **Prefill and Embedded Sending** workflow: + +1. Collecting a name via a web form. +2. Cloning a document template and pre-filling a name field. +3. Opening an embedded sending session so the sender can configure recipients and send for signature. +4. Monitoring invitation statuses. +5. Downloading the final signed document. + +## Technical Flow + +1. **GET Request Handling** + - No `page` parameter: renders the initial name-input form. + - `?page=download-with-status`: renders the status monitoring view. + +2. **Form Submission** + - The backend clones the predefined template, pre-fills the `Name` field, and generates an embedded sending link via `DocumentEmbeddedSendingLinkPost`. + - The frontend is redirected to the embedded sending session. + +3. **Status Polling** + - Frontend polls `/api/samples/PrefillAndEmbeddedSendingAgreement` with `action: 'invite-status'` every 3 seconds. + - Backend fetches field invites via `DocumentGet` and returns JSON statuses. + +4. **Document Download** + - On click, sends `action: 'download'` to fetch the flattened PDF (`DocumentDownloadGet`) and triggers a browser download. + +## Configuration Notes + +- **Template ID**: Defined as `TEMPLATE_ID` in the controller. +- **SignNow SDK**: Uses the official SignNow PHP SDK; credentials loaded from the `.env` configuration. + +____ + +## Ready to build eSignature integrations with SignNow API? Get the SignNow extension for GitHub Copilot + +Use AI-powered code suggestions to generate SignNow API code snippets in your IDE with GitHub Copilot. Get examples for common integration tasks—from authentication and sending documents for signature to handling webhooks, and building branded workflows. + +### **🚀 Why use SignNow with GitHub Copilot** + +* **Relevant code suggestions**: Get AI-powered, up-to-date code snippets for SignNow API calls. All examples reflect the latest API capabilities and follow current best practices. +* **Faster development**: Reduce time spent searching documentation. +* **Fewer mistakes**: Get context-aware guidance aligned with the SignNow API. +* **Smooth onboarding**: Useful for both new and experienced developers working with the API. + +### **Prerequisites:** + +1\. GitHub Copilot installed and enabled. +2\. SignNow account. [Register here](https://www.signnow.com/developers) + +### ⚙️ **How to use it** + +1\. Install the [SignNow extension](https://github.com/apps/signnow). + +2\. Start your prompts with [@signnow](https://github.com/signnow) in the Copilot chat window. The first time you use the extension, you may need to authorize it. + +3\. Enter a prompt describing the integration scenario. +Example: @signnow Generate a PHP code example for prefilling a document and sending it for signature. + +4\. Modify the generated code to match your app's requirements—adjust parameters, headers, and workflows as needed. + +### **Troubleshooting** +**The extension doesn't provide code examples for the SignNow API** + +Make sure you're using `@signnow` in the Copilot chat and that the extension is installed and authorized. +____ + +## Disclaimer + +This sample application is provided **as-is** for demonstration purposes. It is not production-ready and should be reviewed for proper error handling, input validation, security best practices, and customized to fit real-world business requirements. diff --git a/samples/PrefillAndEmbeddedSendingAgreement/SampleController.php b/samples/PrefillAndEmbeddedSendingAgreement/SampleController.php new file mode 100644 index 0000000..29a610a --- /dev/null +++ b/samples/PrefillAndEmbeddedSendingAgreement/SampleController.php @@ -0,0 +1,133 @@ +render(), + 200, + ['Content-Type' => 'text/html'] + ); + } + + public function handlePost(Request $request): Response + { + $action = $request->input('action'); + + $sdk = new Sdk(); + $apiClient = $sdk->build()->authenticate()->getApiClient(); + + if ($action === 'create-embedded-invite') { + $name = $request->input('name'); + $link = $this->createEmbeddedInviteAndReturnSendingLink($apiClient, $name); + return new JsonResponse(['link' => $link]); + } + + if ($action === 'invite-status') { + $documentId = $request->input('document_id'); + $statusList = $this->getDocumentStatuses($apiClient, $documentId); + return new JsonResponse($statusList); + } + + $file = $this->downloadDocument($apiClient, $request->input('document_id')); + $content = file_get_contents($file->getRealPath()); + unlink($file->getRealPath()); + + return new Response($content, 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'attachment; filename="' . $file->getFilename() . '"', + ]); + } + + private function createEmbeddedInviteAndReturnSendingLink(ApiClient $apiClient, string $name): string + { + $document = $this->createDocumentFromTemplate($apiClient, self::TEMPLATE_ID); + $this->prefillFields($apiClient, $document->getId(), ['Name' => $name]); + return $this->getEmbeddedSendingLink($apiClient, $document->getId()); + } + + private function createDocumentFromTemplate(ApiClient $apiClient, string $templateId): CloneTemplatePostResponse + { + $clone = new CloneTemplatePost(); + $clone->withTemplateId($templateId); + return $apiClient->send($clone); + } + + private function getEmbeddedSendingLink(ApiClient $apiClient, string $documentId): string + { + $redirectUrl = config('app.url') + . '/samples/PrefillAndEmbeddedSendingAgreement?page=download-with-status&document_id=' + . $documentId; + + $request = new DocumentEmbeddedSendingLinkPostRequest('document', $redirectUrl, 16); + $request->withDocumentId($documentId); + + /** @var DocumentEmbeddedSendingLinkPostResponse $response */ + $response = $apiClient->send($request); + return $response->getData()->toArray()['url']; + } + + private function prefillFields(ApiClient $apiClient, string $documentId, array $fieldsValue): void + { + $collection = new FieldValueCollection(); + foreach ($fieldsValue as $name => $value) { + if ($value !== null) { + $collection->add(new FieldValue(fieldName: $name, prefilledText: $value)); + } + } + $request = new DocumentPrefillPut($collection); + $request->withDocumentId($documentId); + $apiClient->send($request); + } + + private function downloadDocument(ApiClient $apiClient, string $documentId): SplFileInfo + { + $request = (new DocumentDownloadGet()) + ->withDocumentId($documentId) + ->withType('collapsed'); + $response = $apiClient->send($request); + return $response->getFile(); + } + + private function getDocumentStatuses(ApiClient $apiClient, string $documentId): array + { + /** @var DocumentGetResponse $response */ + $response = $apiClient->send((new DocumentGet())->withDocumentId($documentId)); + $invites = $response->getFieldInvites()->toArray(); + + $statuses = []; + foreach ($invites as $invite) { + $statuses[] = [ + 'name' => $invite['email'] ?? '', + 'timestamp' => isset($invite['updated']) ? date('Y-m-d H:i:s', (int) $invite['updated']) : '', + 'status' => $invite['status'] ?? '', + ]; + } + return $statuses; + } +} diff --git a/samples/PrefillAndEmbeddedSendingAgreement/index.blade.php b/samples/PrefillAndEmbeddedSendingAgreement/index.blade.php new file mode 100644 index 0000000..a2a80e7 --- /dev/null +++ b/samples/PrefillAndEmbeddedSendingAgreement/index.blade.php @@ -0,0 +1,167 @@ + + + + + + + Prefill and Embedded Sending Agreement + + + + + + + + + + + + + + + + + +
+ SignNow Logo +
+ +
+
+

Prefill and Embedded Sending Agreement Form

+
+
+ + +
+ + +
+
+
+ +
+
    +
    + + + + + + + diff --git a/samples/PrefillAndOneClickSendingAgreement/ReadMe.md b/samples/PrefillAndOneClickSendingAgreement/ReadMe.md new file mode 100644 index 0000000..e604e59 --- /dev/null +++ b/samples/PrefillAndOneClickSendingAgreement/ReadMe.md @@ -0,0 +1,73 @@ +# Prefill and One Click Sending Agreement + +## Use Case Overview + +This sample demonstrates how to use the SignNow PHP SDK to implement a **Prefill and One-Click Send** workflow: + +1. Collecting a signer name and email via a web form. +2. Cloning a document template and pre-filling a name field. +3. Sending a field invite directly to the signer's email (one-click send — no embedded sender UI). +4. Monitoring invitation statuses. +5. Downloading the final signed document. + +## Technical Flow + +1. **GET Request Handling** + - No `page` parameter: renders the initial form with name and email fields. + - `?page=status-container`: renders the status monitoring view. + +2. **Form Submission** + - The backend clones the predefined template, pre-fills the `Name` field, resolves the signer role ID, and sends an email invite via `SendInvitePost`. + - The frontend is redirected to the status page with the document ID. + +3. **Status Polling** + - Frontend polls `/api/samples/PrefillAndOneClickSendingAgreement` with `action: 'invite-status'`. + - Backend fetches field invites via `DocumentGet` and returns JSON statuses. + +4. **Document Download** + - On click, sends `action: 'download'` to fetch the flattened PDF (`DocumentDownloadGet`) and triggers a browser download. + +## Configuration Notes + +- **Template ID**: Defined as `TEMPLATE_ID` in the controller. +- **Role Name**: `Recipient 1` — must match the role defined in the template. +- **SignNow SDK**: Uses the official SignNow PHP SDK; credentials loaded from the `.env` configuration. + +____ + +## Ready to build eSignature integrations with SignNow API? Get the SignNow extension for GitHub Copilot + +Use AI-powered code suggestions to generate SignNow API code snippets in your IDE with GitHub Copilot. Get examples for common integration tasks—from authentication and sending documents for signature to handling webhooks, and building branded workflows. + +### **🚀 Why use SignNow with GitHub Copilot** + +* **Relevant code suggestions**: Get AI-powered, up-to-date code snippets for SignNow API calls. All examples reflect the latest API capabilities and follow current best practices. +* **Faster development**: Reduce time spent searching documentation. +* **Fewer mistakes**: Get context-aware guidance aligned with the SignNow API. +* **Smooth onboarding**: Useful for both new and experienced developers working with the API. + +### **Prerequisites:** + +1\. GitHub Copilot installed and enabled. +2\. SignNow account. [Register here](https://www.signnow.com/developers) + +### ⚙️ **How to use it** + +1\. Install the [SignNow extension](https://github.com/apps/signnow). + +2\. Start your prompts with [@signnow](https://github.com/signnow) in the Copilot chat window. The first time you use the extension, you may need to authorize it. + +3\. Enter a prompt describing the integration scenario. +Example: @signnow Generate a PHP code example for sending a one-click invite with prefilled fields. + +4\. Modify the generated code to match your app's requirements—adjust parameters, headers, and workflows as needed. + +### **Troubleshooting** +**The extension doesn't provide code examples for the SignNow API** + +Make sure you're using `@signnow` in the Copilot chat and that the extension is installed and authorized. +____ + +## Disclaimer + +This sample application is provided **as-is** for demonstration purposes. It is not production-ready and should be reviewed for proper error handling, input validation, security best practices, and customized to fit real-world business requirements. diff --git a/samples/PrefillAndOneClickSendingAgreement/SampleController.php b/samples/PrefillAndOneClickSendingAgreement/SampleController.php new file mode 100644 index 0000000..66318f4 --- /dev/null +++ b/samples/PrefillAndOneClickSendingAgreement/SampleController.php @@ -0,0 +1,156 @@ +render(), + 200, + ['Content-Type' => 'text/html'] + ); + } + + public function handlePost(Request $request): Response + { + $action = $request->input('action'); + + $sdk = new Sdk(); + $apiClient = $sdk->build()->authenticate()->getApiClient(); + + if ($action === 'send-invite') { + $name = $request->input('name'); + $email = $request->input('email'); + $documentId = $this->sendInvite($apiClient, $name, $email); + return new JsonResponse(['status' => 'success', 'document_id' => $documentId]); + } + + if ($action === 'invite-status') { + $documentId = $request->input('document_id'); + $statusList = $this->getDocumentStatuses($apiClient, $documentId); + return new JsonResponse($statusList); + } + + $file = $this->downloadDocument($apiClient, $request->input('document_id')); + $content = file_get_contents($file->getRealPath()); + unlink($file->getRealPath()); + + return new Response($content, 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'attachment; filename="' . $file->getFilename() . '"', + ]); + } + + private function sendInvite(ApiClient $apiClient, string $name, string $email): string + { + $document = $this->createDocumentFromTemplate($apiClient, self::TEMPLATE_ID); + $this->prefillFields($apiClient, $document->getId(), ['Name' => $name]); + + $roleId = $this->getRoleId($apiClient, $document->getId(), self::ROLE_NAME); + + $to = new ToCollection(); + $to->add(new To( + email: $email, + roleId: $roleId, + role: 'signer', + order: 1, + subject: 'Subject', + message: 'Message', + )); + + $invite = new SendInvitePost( + documentId: $document->getId(), + to: $to, + from: 'from@email.com', + subject: 'Subject', + message: 'Message', + ); + $apiClient->send($invite); + + return $document->getId(); + } + + private function createDocumentFromTemplate(ApiClient $apiClient, string $templateId): CloneTemplatePostResponse + { + $clone = new CloneTemplatePost(); + $clone->withTemplateId($templateId); + return $apiClient->send($clone); + } + + private function prefillFields(ApiClient $apiClient, string $documentId, array $fieldsValue): void + { + $collection = new FieldValueCollection(); + foreach ($fieldsValue as $name => $value) { + if ($value !== null) { + $collection->add(new FieldValue(fieldName: $name, prefilledText: $value)); + } + } + $request = new DocumentPrefillPut($collection); + $request->withDocumentId($documentId); + $apiClient->send($request); + } + + private function getRoleId(ApiClient $apiClient, string $documentId, string $roleName): string + { + /** @var DocumentGetResponse $response */ + $response = $apiClient->send((new DocumentGet())->withDocumentId($documentId)); + foreach ($response->getRoles() as $role) { + if ($role->getName() === $roleName) { + return $role->getUniqueId(); + } + } + throw new \RuntimeException("Role '{$roleName}' not found."); + } + + private function downloadDocument(ApiClient $apiClient, string $documentId): SplFileInfo + { + $request = (new DocumentDownloadGet()) + ->withDocumentId($documentId) + ->withType('collapsed'); + $response = $apiClient->send($request); + return $response->getFile(); + } + + private function getDocumentStatuses(ApiClient $apiClient, string $documentId): array + { + /** @var DocumentGetResponse $response */ + $response = $apiClient->send((new DocumentGet())->withDocumentId($documentId)); + $invites = $response->getFieldInvites()->toArray(); + + $statuses = []; + foreach ($invites as $invite) { + $statuses[] = [ + 'name' => $invite['email'] ?? '', + 'timestamp' => isset($invite['updated']) ? date('Y-m-d H:i:s', (int) $invite['updated']) : '', + 'status' => $invite['status'] ?? '', + ]; + } + return $statuses; + } +} diff --git a/samples/PrefillAndOneClickSendingAgreement/index.blade.php b/samples/PrefillAndOneClickSendingAgreement/index.blade.php new file mode 100644 index 0000000..4817c8e --- /dev/null +++ b/samples/PrefillAndOneClickSendingAgreement/index.blade.php @@ -0,0 +1,174 @@ + + + + + + + Prefill and One Click Sending Agreement + + + + + + + + + + + + + + + + + +
    + SignNow Logo +
    + +
    +
    +

    Prefill and One Click Sending Agreement Form

    +
    +
    + + +
    +
    + + +
    + + +
    +
    +
    + + + + + + + + + diff --git a/samples/UploadEmbeddedSender/SampleController.php b/samples/UploadEmbeddedSender/SampleController.php index 23ee745..01c986e 100644 --- a/samples/UploadEmbeddedSender/SampleController.php +++ b/samples/UploadEmbeddedSender/SampleController.php @@ -27,6 +27,7 @@ use SignNow\Api\DocumentGroup\Request\DocumentGroupRecipientsGet; use SignNow\Api\DocumentGroup\Response\DocumentGroupRecipientsGet as DocumentGroupRecipientsGetResponse; use SignNow\ApiClient; +use SignNow\Exception\SignNowApiException; use SignNow\Sdk; use Symfony\Component\HttpFoundation\Response; use SplFileInfo; @@ -48,10 +49,14 @@ public function handlePost(Request $request): Response { $action = $request->input('action'); - $sdk = new Sdk(); - $apiClient = $sdk->build() - ->authenticate() - ->getApiClient(); + try { + $sdk = new Sdk(); + $apiClient = $sdk->build() + ->authenticate() + ->getApiClient(); + } catch (SignNowApiException $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } switch ($action) { case 'upload_and_create_dg': @@ -67,26 +72,30 @@ public function handlePost(Request $request): Response private function uploadAndCreateDocumentGroup(Request $request, ApiClient $apiClient): JsonResponse { - // 1. Upload PDF file to SignNow - $documentResponse = $this->uploadDocument($apiClient); - if (!$documentResponse['success']) { - return response()->json($documentResponse, 500); - } + try { + // 1. Upload PDF file to SignNow + $documentResponse = $this->uploadDocument($apiClient); + if (!$documentResponse['success']) { + return response()->json($documentResponse, 500); + } - $documentId = $documentResponse['document_id']; + $documentId = $documentResponse['document_id']; - // 2. Create Document Group from uploaded document - $documentGroupResponse = $this->createDocumentGroup($apiClient, $documentId); - if (!$documentGroupResponse['success']) { - return response()->json($documentGroupResponse, 500); - } + // 2. Create Document Group from uploaded document + $documentGroupResponse = $this->createDocumentGroup($apiClient, $documentId); + if (!$documentGroupResponse['success']) { + return response()->json($documentGroupResponse, 500); + } - $documentGroupId = $documentGroupResponse['document_group_id']; + $documentGroupId = $documentGroupResponse['document_group_id']; - // 3. Create embedded sending link - $embeddedSendingResponse = $this->createEmbeddedSendingUrl($apiClient, $documentGroupId); - if (!$embeddedSendingResponse['success']) { - return response()->json($embeddedSendingResponse, 500); + // 3. Create embedded sending link + $embeddedSendingResponse = $this->createEmbeddedSendingUrl($apiClient, $documentGroupId); + if (!$embeddedSendingResponse['success']) { + return response()->json($embeddedSendingResponse, 500); + } + } catch (SignNowApiException $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } return response()->json([ @@ -175,7 +184,11 @@ private function createEmbeddedSendingUrl(ApiClient $apiClient, string $document private function getInviteStatus(Request $request, ApiClient $apiClient): JsonResponse { $documentGroupId = $request->input('document_group_id'); - $signers = $this->getDocumentGroupSignersStatus($apiClient, $documentGroupId); + try { + $signers = $this->getDocumentGroupSignersStatus($apiClient, $documentGroupId); + } catch (SignNowApiException $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } return response()->json($signers); } @@ -183,7 +196,11 @@ private function downloadDocumentGroup(Request $request, ApiClient $apiClient): { $documentGroupId = $request->input('document_group_id'); - $file = $this->downloadDocumentGroupFile($apiClient, $documentGroupId); + try { + $file = $this->downloadDocumentGroupFile($apiClient, $documentGroupId); + } catch (SignNowApiException $e) { + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } $content = file_get_contents($file->getRealPath()); unlink($file->getRealPath()); diff --git a/samples_witelist.json b/samples_witelist.json index c8fc2db..28a3eae 100644 --- a/samples_witelist.json +++ b/samples_witelist.json @@ -1,5 +1,5 @@ [ - "EVDemoSendingAnd3EmbededSigners", + "EVDemoSendingAnd3EmbeddedSigners", "EmbeddedSenderWithFormCreditLoanAgreement", "EmbeddedSenderWithoutFormFile", "EmbeddedSignerConsentForm", @@ -16,5 +16,7 @@ "ISVWithFormAndOneClickSendMergeFields", "HROnboardingSystem", "UploadEmbeddedSender", - "UploadEmbeddedEditingAndInvite" -] \ No newline at end of file + "UploadEmbeddedEditingAndInvite", + "PrefillAndEmbeddedSendingAgreement", + "PrefillAndOneClickSendingAgreement" +]