diff --git a/.env.example b/.env.example index 7015999..52e7665 100644 --- a/.env.example +++ b/.env.example @@ -2,9 +2,26 @@ ## signNow API SDK configuration ## -## Replace these dummy values with your actual credentials except SIGNNOW_API_HOST +# Required — replace placeholders with real credentials before running. SIGNNOW_API_HOST=https://api.signnow.com -SIGNNOW_API_BASIC_TOKEN=test -SIGNNOW_API_USERNAME=test -SIGNNOW_API_PASSWORD=test -SIGNNOW_DOWNLOADS_DIR=~/Downloads +SIGNNOW_API_BASIC_TOKEN= +SIGNNOW_API_USERNAME= +SIGNNOW_API_PASSWORD= + +# Path inside the running environment where downloaded files are written. +# Inside Docker, leave as /tmp (or another writable path). The ~ does not expand in containers. +SIGNNOW_DOWNLOADS_DIR=/tmp + +## +## Sample-app runtime configuration +## + +# Public base URL used to build redirect URIs sent to the SignNow embed widget. +# Must match the host the browser reaches this app on. +APP_BASE_URL=http://localhost:8080 + +# Demo-only signer credentials used by samples that generate limited signer tokens. +# For production deployments replace with per-tenant values or remove the affected samples. +SIGNNOW_DEMO_USER_EMAIL=example@example.com +SIGNNOW_DEMO_USER_PASSWORD=example +SIGNNOW_SIGNER_EMAIL=test@example.com diff --git a/Dockerfile b/Dockerfile index 89d56fd..2018677 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,21 @@ -FROM maven:3.9.9-eclipse-temurin-23 AS build +FROM maven:3.9.9-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml ./ +RUN mvn -B dependency:go-offline COPY src ./src -RUN mvn clean install package -DskipTests +RUN mvn -B clean package -DskipTests -FROM openjdk:23-jdk-slim -VOLUME /tmp -COPY --from=build /app/target/java-sample-app-0.0.1-SNAPSHOT.jar app.jar -ENTRYPOINT ["java", "-jar", "/app.jar"] +FROM eclipse-temurin:17-jre +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd -r app && useradd -r -g app -d /home/app -m app +WORKDIR /home/app +COPY --from=build /app/target/java-sample-app-0.0.1-SNAPSHOT.jar /home/app/app.jar +RUN chown -R app:app /home/app +USER app +ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+UseG1GC" +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \ + CMD curl -fsS http://127.0.0.1:8080/actuator/health || exit 1 +ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /home/app/app.jar"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..dd4e228 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index ad8c45f..31e3dd2 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,197 @@ -# SignNow Java SDK Sample Application +# SignNow Java Sample App -This is a demonstration project that showcases how to use the [SignNow Java SDK](https://github.com/signnow/SNJavaSDK) to interact with the SignNow API. +[![Java](https://img.shields.io/badge/java-17-orange)](https://openjdk.org/) +[![Spring Boot](https://img.shields.io/badge/Spring_Boot-3.4-brightgreen)](https://spring.io/projects/spring-boot) +[![SignNow SDK](https://img.shields.io/badge/SignNow_SDK-3.5+-light)](https://github.com/signnow/SNJavaSDK) +[![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE) -Each sample represents a standalone use case and is implemented in a separate directory under: +A Spring Boot application demonstrating the SignNow API via the official [SignNow Java SDK](https://github.com/signnow/SNJavaSDK). -``` -src/main/java/com/signnow/samples -``` +## Quick Start -The static frontend resources for each sample (e.g. HTML, JS, CSS) are located in: +### 1. Enter the directory -``` -src/main/resources/static/samples +```bash +cd SampleApps/java-sample-app ``` -## Getting Started - -### 1. Set Up Your Environment - -Copy the provided `.env.example` file to `.env`: +### 2. Configure environment ```bash cp .env.example .env ``` -Edit the `.env` file and replace the placeholder values with your actual SignNow API credentials and settings. +Edit `.env` and fill in your SignNow credentials: -### 2. Build and Run the Application +| Variable | Example | Description | +|---|---|---| +| `SIGNNOW_API_HOST` | `https://api.signnow.com` | Production or sandbox URL | +| `SIGNNOW_API_BASIC_TOKEN` | `c2lnbk5vdy4...` | Base64 token from [API Dashboard](https://app.signnow.com/webapp/api-dashboard/keys) | +| `SIGNNOW_API_USERNAME` | `you@example.com` | Your SignNow account email | +| `SIGNNOW_API_PASSWORD` | `••••••` | Your SignNow account password | +| `SIGNNOW_DOWNLOADS_DIR` | `/tmp/signnow-downloads` | Where downloaded documents are cached | +| `APP_BASE_URL` | `http://localhost:8080` | Public base URL used in `redirect_uri` | +| `SIGNNOW_SIGNER_EMAIL` | `signer@example.com` | Default embedded signer email | +| `SIGNNOW_DEMO_USER_EMAIL` | `demo@example.com` | Demo sender email (used in some samples) | +| `SIGNNOW_DEMO_USER_PASSWORD` | `••••••` | Demo sender password | -You can build and run the application using Docker: +### 3. Run with Docker ```bash docker build -t java-sample-app . docker run -v $(pwd)/.env:/.env -p 8080:8080 java-sample-app ``` -Once the app is running, access it via: +### 4. Run locally (no Docker) -``` -http://localhost:8080/samples/{exampleName} +```bash +mvn spring-boot:run ``` -Replace `{exampleName}` with the name of the specific example you want to run (e.g. `DocumentInvite`, `PrefillForm`, etc.). +Requires Java 17+ and Maven 3.9+ on `PATH`. The app reads `.env` from the working directory. -## Routing Logic +### 5. Open a sample -All requests are routed through a single controller: `RoutingController`. +``` +http://localhost:8080/samples/EmbeddedSignerConsentForm +``` -* **GET /samples/{exampleName}**: Loads and executes the corresponding `IndexController` class from the example folder. -* **POST /api/samples/{exampleName}**: Handles form submissions for the given example. +`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 -Only example names matching `^[a-zA-Z0-9_]+$` are allowed for security reasons. +``` +src/main/java/com/signnow/ + javasampleapp/ + JavaSampleAppApplication.java Spring Boot entry point + ExampleInterface.java Sample contract (handleGet + handlePost) + controllers/ + RoutingController.java GET /samples/:name, POST /api/samples/:name + config/ + AppConfig.java Reads .env; provides AppConfig.sampleUrl() + samples/ + / + IndexController.java Implements ExampleInterface + +src/main/resources/ + static/ + samples// + index.html UI served on GET /samples/ + css/, img/ Shared static assets + application.properties Spring config + +src/test/ + JavaSampleAppApplicationTests.java Context load test + controllers/RoutingControllerTest.java MockMvc routing tests +``` -## Structure of a Sample Example +## Routing + +| Method | Path | Handler | +|---|---|---| +| GET | `/samples/{name}` | `samples//IndexController.handleGet` | +| POST | `/api/samples/{name}` | `samples//IndexController.handlePost` | +| GET | `/css/*`, `/img/*`, etc. | Shared static files | + +Sample names must match `^[a-zA-Z0-9_]+$`. `RoutingController` loads each sample's `IndexController` by class name convention at request time — no whitelist or registration needed. + +## Add a New Sample + +1. Create `src/main/java/com/signnow/samples/MyNewSample/IndexController.java`: + ```java + package com.signnow.samples.MyNewSample; + + import com.signnow.javasampleapp.ExampleInterface; + import org.springframework.http.ResponseEntity; + import org.springframework.stereotype.Controller; + import java.io.IOException; + import java.util.Map; + + @Controller + public class IndexController implements ExampleInterface { + @Override + public ResponseEntity handleGet(Map queryParams) throws IOException { + try (var in = getClass().getResourceAsStream("/static/samples/MyNewSample/index.html")) { + return ResponseEntity.ok().header("Content-Type", "text/html") + .body(new String(in.readAllBytes())); + } + } + + @Override + public ResponseEntity handlePost(String formData) throws Exception { + // parse formData as JSON and implement your logic + return ResponseEntity.ok("{}"); + } + } + ``` +2. Create `src/main/resources/static/samples/MyNewSample/index.html`. +3. Restart the application — `RoutingController` discovers the new controller automatically. + +## Tests -Each sample should implement the `ExampleInterface` and provide its own `IndexController` class with `handleGet()` and `handlePost()` methods. +```bash +mvn test +``` -____ +2 test classes: +- `JavaSampleAppApplicationTests` — Spring context loads without errors +- `RoutingControllerTest` — 6 MockMvc tests covering name validation (`^[a-zA-Z0-9_]+$`), 404 for unknown samples, and routing dispatch -## Ready to build eSignature integrations with SignNow API? Get the SignNow extension for GitHub Copilot +Tests do **not** exercise real SignNow API calls (live credentials required). -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. +## SDK Notes -### **🚀 Why use SignNow with GitHub Copilot** +Known constraints in the SignNow Java SDK that affect this codebase: -* **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. +**No multipart support in `RoutingController`** — `handlePost` receives `@RequestBody String formData` (raw JSON), not multipart. Samples that conceptually "upload" a file instead read a pre-bundled PDF from `src/main/resources/static/samples//`. See `UploadEmbeddedSender` for the pattern. -### **Prerequisites:** +**`Invite` constructor order** — `new Invite(email, roleId, order, firstName, lastName)`. There is no shorter overload; all five parameters are required. -1\. GitHub Copilot installed and enabled. -2\. SignNow account. [Register here](https://www.signnow.com/developers) +**`DocumentInvitePostResponse.getData()`** — Returns a collection; each item exposes `.getRoleId()` (to match against role IDs you looked up) and `.getId()` (the invite ID needed for `DocumentInviteLinkPostRequest`). -### ⚙️ **How to use it** +**`DocumentInviteLinkPostRequest` constructor** — `(authType, linkExpiration)` where `authType` is typically `"none"`. Chain `.withDocumentId()` and `.withFieldInviteId()` before sending. -1\. Install the [SignNow extension](https://github.com/apps/signnow). +**`DocumentEmbeddedEditorLinkPostRequest` constructor** — `(redirectUri, redirectTarget, linkExpiration)`. Chain `.withDocumentId()` before sending. Response: `getData().getUrl()`. -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. +## Tech Stack -3\. Enter a prompt describing the integration scenario. -Example: @signnow Generate a Java code example for sending a document group to two signers. +- Java 17 +- Spring Boot 3.4.1 +- `signnow-java-sdk` 3.5.1 (from Maven Central) +- Maven 3.9 +- JUnit 5 + Spring MockMvc (tests) +- Docker: multi-stage `maven:3.9-eclipse-temurin-17` → `eclipse-temurin:17-jre` -4\. Modify the generated code to match your app’s requirements—adjust parameters, headers, and workflows as needed. +## GitHub Copilot Extension -### **Troubleshooting** -**The extension doesn’t provide code examples for the SignNow API** +Get AI-powered SignNow code suggestions in your IDE: +[github.com/apps/signnow](https://github.com/apps/signnow) — start prompts with `@signnow`. -Make sure you're using `@signnow` in the Copilot chat and that the extension is installed and authorized. +## License -____ +See [LICENSE](./LICENSE). diff --git a/pom.xml b/pom.xml index 73260f4..776aa61 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-actuator + org.springframework.boot spring-boot-starter-test @@ -42,7 +46,7 @@ com.signnow signnow-java-sdk - 3.5.0 + 3.5.1 diff --git a/src/main/java/com/signnow/javasampleapp/config/AppConfig.java b/src/main/java/com/signnow/javasampleapp/config/AppConfig.java new file mode 100644 index 0000000..12dcd1f --- /dev/null +++ b/src/main/java/com/signnow/javasampleapp/config/AppConfig.java @@ -0,0 +1,57 @@ +package com.signnow.javasampleapp.config; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Runtime config holder for sample controllers that are instantiated via reflection + * and cannot receive Spring-injected dependencies. Spring populates the static fields + * at startup through {@link Holder}; controllers read them via the static accessors. + */ +public final class AppConfig { + + private AppConfig() {} + + private static volatile String baseUrl = "http://localhost:8080"; + private static volatile String demoUserEmail = "example@example.com"; + private static volatile String demoUserPassword = "example"; + + public static String baseUrl() { + return baseUrl; + } + + public static String sampleUrl(String exampleName) { + return baseUrl + "/samples/" + exampleName; + } + + public static String demoUserEmail() { + return demoUserEmail; + } + + public static String demoUserPassword() { + return demoUserPassword; + } + + @Component + static class Holder { + + Holder(@Value("${app.base-url:http://localhost:8080}") String baseUrl, + @Value("${signnow.api.demo_user_email:example@example.com}") String demoUserEmail, + @Value("${signnow.api.demo_user_password:example}") String demoUserPassword) { + AppConfig.baseUrl = trimTrailingSlash(baseUrl); + AppConfig.demoUserEmail = demoUserEmail; + AppConfig.demoUserPassword = demoUserPassword; + } + + @PostConstruct + void ready() { + // Touch volatile fields to publish writes; initialization happens in the constructor. + } + + private static String trimTrailingSlash(String url) { + if (url == null || url.isEmpty()) return url; + return url.endsWith("/") ? url.substring(0, url.length() - 1) : url; + } + } +} diff --git a/src/main/java/com/signnow/javasampleapp/config/SecurityHeadersFilter.java b/src/main/java/com/signnow/javasampleapp/config/SecurityHeadersFilter.java new file mode 100644 index 0000000..26b039b --- /dev/null +++ b/src/main/java/com/signnow/javasampleapp/config/SecurityHeadersFilter.java @@ -0,0 +1,31 @@ +package com.signnow.javasampleapp.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class SecurityHeadersFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + response.setHeader("X-Content-Type-Options", "nosniff"); + response.setHeader("X-Frame-Options", "SAMEORIGIN"); + response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); + response.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=()"); + if (request.isSecure()) { + response.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/signnow/javasampleapp/controllers/GlobalExceptionHandler.java b/src/main/java/com/signnow/javasampleapp/controllers/GlobalExceptionHandler.java new file mode 100644 index 0000000..306ec6e --- /dev/null +++ b/src/main/java/com/signnow/javasampleapp/controllers/GlobalExceptionHandler.java @@ -0,0 +1,55 @@ +package com.signnow.javasampleapp.controllers; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.signnow.core.exception.SignNowApiException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.LinkedHashMap; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleMalformedBody(HttpMessageNotReadableException e) { + log.warn("Malformed request body: {}", e.getMostSpecificCause().getMessage()); + return problem(HttpStatus.BAD_REQUEST, "Malformed request body"); + } + + @ExceptionHandler({JsonProcessingException.class, IllegalArgumentException.class, ClassCastException.class}) + public ResponseEntity> handleBadInput(Exception e) { + log.warn("Rejected request due to invalid input: {}", e.getMessage()); + return problem(HttpStatus.BAD_REQUEST, "Invalid request payload"); + } + + @ExceptionHandler(SignNowApiException.class) + public ResponseEntity> handleSignNowApi(SignNowApiException e) { + log.error("SignNow API call failed", e); + return problem(HttpStatus.BAD_GATEWAY, "Upstream SignNow API error"); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleUnexpected(Exception e) { + log.error("Unhandled exception", e); + return problem(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"); + } + + private ResponseEntity> problem(HttpStatus status, String message) { + Map body = new LinkedHashMap<>(); + body.put("success", false); + body.put("status", status.value()); + body.put("message", message); + return ResponseEntity.status(status) + .contentType(MediaType.APPLICATION_JSON) + .body(body); + } +} diff --git a/src/main/java/com/signnow/javasampleapp/controllers/RoutingController.java b/src/main/java/com/signnow/javasampleapp/controllers/RoutingController.java index e6475d1..5414b62 100644 --- a/src/main/java/com/signnow/javasampleapp/controllers/RoutingController.java +++ b/src/main/java/com/signnow/javasampleapp/controllers/RoutingController.java @@ -1,7 +1,11 @@ package com.signnow.javasampleapp.controllers; import com.fasterxml.jackson.databind.ObjectMapper; +import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @@ -9,107 +13,91 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.regex.Pattern; @Controller public class RoutingController { + private static final Logger log = LoggerFactory.getLogger(RoutingController.class); + + private static final Pattern EXAMPLE_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+$"); + private static final String CONTROLLER_PACKAGE = "com.signnow.samples."; + private static final String CONTROLLER_CLASS = ".IndexController"; + private static final String FALLBACK_ERROR_HTML = + "

404 - Page Not Found

"; + @GetMapping("/") public ResponseEntity root() throws IOException { - try (var inputStream = getClass().getResourceAsStream("/static/error.html")) { - if (inputStream == null) { - return ResponseEntity.status(404) - .header("Content-Type", "text/html") - .body("

404 - Page Not Found

"); - } - String html = new String(inputStream.readAllBytes()); - return ResponseEntity.status(404) - .header("Content-Type", "text/html") - .body(html); - } + return notFound(); } @GetMapping("/samples/{exampleName}") public ResponseEntity routeExample( @PathVariable String exampleName, @RequestParam Map queryParams - ) throws IOException { - if (!exampleName.matches("^[a-zA-Z0-9_]+$")) { - try (var inputStream = getClass().getResourceAsStream("/static/error.html")) { - if (inputStream == null) { - return ResponseEntity.status(404) - .header("Content-Type", "text/html") - .body("

404 - Page Not Found

"); - } - String html = new String(inputStream.readAllBytes()); - return ResponseEntity.status(404) - .header("Content-Type", "text/html") - .body(html); - } + ) throws IOException, SignNowApiException { + if (!EXAMPLE_NAME_PATTERN.matcher(exampleName).matches()) { + log.warn("Rejected sample GET with invalid name: {}", exampleName); + return notFound(); } - String controllerPath = "com.signnow.samples." + exampleName + ".IndexController"; try { - Class controllerClass = Class.forName(controllerPath); - Object controllerInstance = controllerClass.getDeclaredConstructor().newInstance(); - if (controllerInstance instanceof ExampleInterface) { - return ((ExampleInterface) controllerInstance).handleGet(queryParams); + ExampleInterface controller = loadExampleController(exampleName); + if (controller != null) { + return controller.handleGet(queryParams); } + } catch (ReflectiveOperationException e) { + log.error("Failed to instantiate example controller '{}'", exampleName, e); } catch (Exception e) { - // Log the error - System.err.println("Error loading example controller: " + e.getMessage()); - } - try (var inputStream = getClass().getResourceAsStream("/static/error.html")) { - if (inputStream == null) { - return ResponseEntity.status(404) - .header("Content-Type", "text/html") - .body("

404 - Page Not Found

"); - } - String html = new String(inputStream.readAllBytes()); - return ResponseEntity.status(404) - .header("Content-Type", "text/html") - .body(html); + log.error("Example '{}' failed to handle GET", exampleName, e); + throw e; } + return notFound(); } @PostMapping("/api/samples/{exampleName}") public ResponseEntity handleFormSubmission( @PathVariable String exampleName, - @RequestBody String formData) throws IOException { - if (!exampleName.matches("^[a-zA-Z0-9_]+$")) { - try (var inputStream = getClass().getResourceAsStream("/static/error.html")) { - if (inputStream == null) { - return ResponseEntity.status(404) - .header("Content-Type", "text/html") - .body("

404 - Page Not Found

"); - } - String html = new String(inputStream.readAllBytes()); - return ResponseEntity.status(404) - .header("Content-Type", "text/html") - .body(html); - } + @RequestBody String formData) throws IOException, SignNowApiException { + if (!EXAMPLE_NAME_PATTERN.matcher(exampleName).matches()) { + log.warn("Rejected sample POST with invalid name: {}", exampleName); + return notFound(); } - String controllerPath = "com.signnow.samples." + exampleName + ".IndexController"; try { - Class controllerClass = Class.forName(controllerPath); - Object controllerInstance = controllerClass.getDeclaredConstructor().newInstance(); - if (controllerInstance instanceof ExampleInterface) { - return ((ExampleInterface) controllerInstance).handlePost(formData); + ExampleInterface controller = loadExampleController(exampleName); + if (controller != null) { + return controller.handlePost(formData); } - } catch (Exception e) { + } catch (ReflectiveOperationException e) { + log.error("Failed to instantiate example controller '{}'", exampleName, e); Map errorResponse = new HashMap<>(); - errorResponse.put("error", e.getMessage()); + errorResponse.put("error", "Example not available"); return ResponseEntity.status(500) - .header("Content-Type", "application/json") + .contentType(MediaType.APPLICATION_JSON) .body(new ObjectMapper().writeValueAsString(errorResponse)); + } catch (Exception e) { + log.error("Example '{}' failed to handle POST", exampleName, e); + throw e; } + return notFound(); + } + + private ExampleInterface loadExampleController(String exampleName) throws ReflectiveOperationException { + String controllerPath = CONTROLLER_PACKAGE + exampleName + CONTROLLER_CLASS; + Class controllerClass = Class.forName(controllerPath); + Object controllerInstance = controllerClass.getDeclaredConstructor().newInstance(); + if (controllerInstance instanceof ExampleInterface example) { + return example; + } + return null; + } + + private ResponseEntity notFound() throws IOException { try (var inputStream = getClass().getResourceAsStream("/static/error.html")) { - if (inputStream == null) { - return ResponseEntity.status(404) - .header("Content-Type", "text/html") - .body("

404 - Page Not Found

"); - } - String html = new String(inputStream.readAllBytes()); + String html = inputStream == null + ? FALLBACK_ERROR_HTML + : new String(inputStream.readAllBytes()); return ResponseEntity.status(404) - .header("Content-Type", "text/html") + .contentType(MediaType.TEXT_HTML) .body(html); } } diff --git a/src/main/java/com/signnow/samples/EVDemoSendingAnd3EmbeddedSigners/IndexController.java b/src/main/java/com/signnow/samples/EVDemoSendingAnd3EmbeddedSigners/IndexController.java new file mode 100644 index 0000000..b394649 --- /dev/null +++ b/src/main/java/com/signnow/samples/EVDemoSendingAnd3EmbeddedSigners/IndexController.java @@ -0,0 +1,243 @@ +package com.signnow.samples.EVDemoSendingAnd3EmbeddedSigners; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.signnow.Sdk; +import com.signnow.api.document.request.DocumentDownloadGetRequest; +import com.signnow.api.document.response.DocumentDownloadGetResponse; +import com.signnow.api.document.request.DocumentGetRequest; +import com.signnow.api.document.response.DocumentGetResponse; +import com.signnow.api.documentfield.request.DocumentPrefillPutRequest; +import com.signnow.api.documentfield.request.data.Field; +import com.signnow.api.documentfield.request.data.FieldCollection; +import com.signnow.api.embeddedinvite.request.DocumentInviteLinkPostRequest; +import com.signnow.api.embeddedinvite.request.DocumentInvitePostRequest; +import com.signnow.api.embeddedinvite.request.data.Invite; +import com.signnow.api.embeddedinvite.request.data.InviteCollection; +import com.signnow.api.embeddedinvite.response.DocumentInviteLinkPostResponse; +import com.signnow.api.embeddedinvite.response.DocumentInvitePostResponse; +import com.signnow.api.template.request.CloneTemplatePostRequest; +import com.signnow.api.template.response.CloneTemplatePostResponse; +import com.signnow.core.ApiClient; +import com.signnow.core.exception.SignNowApiException; +import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; + +import java.io.File; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +@Controller +public class IndexController implements ExampleInterface { + + private static final String TEMPLATE_ID = "34009a3d21b5468d86d886cd715658c453335c61"; + private static final String SAMPLE_NAME = "EVDemoSendingAnd3EmbeddedSigners"; + + @Override + public ResponseEntity handleGet(Map queryParams) throws IOException { + try (var inputStream = getClass().getResourceAsStream("/static/samples/" + SAMPLE_NAME + "/index.html")) { + if (inputStream == null) { + throw new IOException("HTML file not found in classpath"); + } + String html = new String(inputStream.readAllBytes()); + return ResponseEntity.ok().header("Content-Type", "text/html").body(html); + } + } + + @Override + public ResponseEntity handlePost(String formData) throws IOException, SignNowApiException { + Map data = new ObjectMapper().readValue(formData, Map.class); + String action = (String) data.getOrDefault("action", ""); + + ApiClient client = new Sdk().build().authenticate().getApiClient(); + + switch (action) { + case "start-workflow": + return startWorkflow(client, data); + case "next-signer": + return nextSigner(client, data); + case "download": + return download(client, data); + case "invite-status": + return inviteStatus(client, data); + default: + Map error = new HashMap<>(); + error.put("error", "Invalid action"); + return ResponseEntity.badRequest().body(new ObjectMapper().writeValueAsString(error)); + } + } + + private ResponseEntity startWorkflow(ApiClient client, Map data) throws SignNowApiException, IOException { + String agentName = (String) data.getOrDefault("agent_name", ""); + String agentEmail = (String) data.getOrDefault("agent_email", ""); + String signer1Name = (String) data.getOrDefault("signer1_name", ""); + String signer1Email = (String) data.getOrDefault("signer1_email", ""); + String signer2Name = (String) data.getOrDefault("signer2_name", ""); + String signer2Email = (String) data.getOrDefault("signer2_email", ""); + + // 1. Clone template + CloneTemplatePostRequest cloneReq = new CloneTemplatePostRequest(); + cloneReq.withTemplateId(TEMPLATE_ID); + CloneTemplatePostResponse cloneResp = (CloneTemplatePostResponse) client.send(cloneReq).getResponse(); + String documentId = cloneResp.getId(); + + // 2. Prefill signer name fields + FieldCollection fields = new FieldCollection(); + if (!signer1Name.isEmpty()) { + fields.add(new Field("Signer 1 Name", signer1Name)); + fields.add(new Field("Text Field 18", signer1Name)); + } + if (!signer2Name.isEmpty()) { + fields.add(new Field("Signer 2 Name", signer2Name)); + fields.add(new Field("Text Field 19", signer2Name)); + } + DocumentPrefillPutRequest prefillReq = new DocumentPrefillPutRequest(fields); + prefillReq.withDocumentId(documentId); + client.send(prefillReq); + + // 3. Get role IDs + String agentRoleId = getRoleId(client, documentId, "Contract Preparer"); + String signer1RoleId = getRoleId(client, documentId, "Recipient 1"); + String signer2RoleId = getRoleId(client, documentId, "Recipient 2"); + + // 4. Create embedded invites for all three roles + InviteCollection invites = new InviteCollection(); + invites.add(new Invite(agentEmail, agentRoleId, 1, firstName(agentName), lastName(agentName))); + invites.add(new Invite(signer1Email, signer1RoleId, 2, firstName(signer1Name), lastName(signer1Name))); + invites.add(new Invite(signer2Email, signer2RoleId, 3, firstName(signer2Name), lastName(signer2Name))); + + DocumentInvitePostRequest inviteReq = new DocumentInvitePostRequest(invites, null); + inviteReq.withDocumentId(documentId); + DocumentInvitePostResponse inviteResp = (DocumentInvitePostResponse) client.send(inviteReq).getResponse(); + + // 5. Build roleId → inviteId map + Map inviteMap = new HashMap<>(); + for (var item : inviteResp.getData()) { + inviteMap.put(item.getRoleId(), item.getId()); + } + + // 6. Get invite link for agent (first signer) + String agentInviteId = inviteMap.get(agentRoleId); + String redirectUrl = makeRedirectUrl(documentId, "signer1"); + String agentLink = getInviteLink(client, documentId, agentInviteId, redirectUrl); + + Map result = new HashMap<>(); + result.put("document_id", documentId); + result.put("embedded_link", agentLink); + result.put("message", "Agent embedded signing link created. Agent can now sign."); + return ResponseEntity.ok().header("Content-Type", "application/json") + .body(new ObjectMapper().writeValueAsString(result)); + } + + private ResponseEntity nextSigner(ApiClient client, Map data) throws SignNowApiException, IOException { + String documentId = (String) data.get("document_id"); + String roleName = (String) data.get("roleName"); + String redirectKey = "Recipient 1".equals(roleName) ? "signer2" : "finish"; + + String inviteId = getInviteIdForRoleName(client, documentId, roleName); + String signingLink = getInviteLink(client, documentId, inviteId, makeRedirectUrl(documentId, redirectKey)); + + Map result = new HashMap<>(); + result.put("embedded_link", signingLink); + result.put("message", "Embedded link for " + roleName + " created. Ready for signing."); + return ResponseEntity.ok().header("Content-Type", "application/json") + .body(new ObjectMapper().writeValueAsString(result)); + } + + private ResponseEntity download(ApiClient client, Map data) throws SignNowApiException, IOException { + String documentId = (String) data.get("document_id"); + File file = downloadDocument(client, documentId); + String filename = file.getName(); + byte[] content = Files.readAllBytes(file.toPath()); + file.delete(); + return ResponseEntity.ok() + .header("Content-Type", "application/pdf") + .header("Content-Disposition", "attachment; filename=\"" + filename + "\"") + .body(content); + } + + private ResponseEntity inviteStatus(ApiClient client, Map data) throws SignNowApiException, IOException { + String documentId = (String) data.get("document_id"); + + DocumentGetRequest docReq = new DocumentGetRequest(); + docReq.withDocumentId(documentId); + DocumentGetResponse docResp = (DocumentGetResponse) client.send(docReq).getResponse(); + + List> statuses = new LinkedList<>(); + for (var invite : docResp.getFieldInvites()) { + Map entry = new HashMap<>(); + entry.put("email", invite.getEmail()); + entry.put("status", invite.getStatus()); + entry.put("timestamp", ""); + statuses.add(entry); + } + return ResponseEntity.ok().header("Content-Type", "application/json") + .body(new ObjectMapper().writeValueAsString(statuses)); + } + + // --- helpers --- + + private String getRoleId(ApiClient client, String documentId, String roleName) throws SignNowApiException { + DocumentGetRequest req = new DocumentGetRequest(); + req.withDocumentId(documentId); + DocumentGetResponse resp = (DocumentGetResponse) client.send(req).getResponse(); + return resp.getRoles().stream() + .filter(r -> roleName.equals(r.getName())) + .findFirst() + .map(r -> r.getUniqueId()) + .orElseThrow(() -> new IllegalArgumentException("Role '" + roleName + "' not found")); + } + + private String getInviteLink(ApiClient client, String documentId, String inviteId, String redirectUrl) + throws SignNowApiException, IOException { + DocumentInviteLinkPostRequest linkReq = new DocumentInviteLinkPostRequest("none", 15); + linkReq.withFieldInviteId(inviteId); + linkReq.withDocumentId(documentId); + DocumentInviteLinkPostResponse linkResp = (DocumentInviteLinkPostResponse) client.send(linkReq).getResponse(); + return linkResp.getData().getLink() + "&redirect_uri=" + URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8); + } + + private String getInviteIdForRoleName(ApiClient client, String documentId, String roleName) + throws SignNowApiException { + String roleId = getRoleId(client, documentId, roleName); + DocumentGetRequest req = new DocumentGetRequest(); + req.withDocumentId(documentId); + DocumentGetResponse resp = (DocumentGetResponse) client.send(req).getResponse(); + for (var invite : resp.getFieldInvites()) { + if (roleId.equals(invite.getRoleId())) { + return invite.getId(); + } + } + throw new IllegalArgumentException("Invite for role '" + roleName + "' not found"); + } + + private File downloadDocument(ApiClient client, String documentId) throws SignNowApiException { + DocumentDownloadGetRequest req = new DocumentDownloadGetRequest(); + req.withDocumentId(documentId).withType("collapsed"); + DocumentDownloadGetResponse resp = (DocumentDownloadGetResponse) client.send(req).getResponse(); + return resp.getFile(); + } + + private String makeRedirectUrl(String documentId, String nextStep) { + return AppConfig.sampleUrl(SAMPLE_NAME) + "?document_id=" + documentId + "&step=" + nextStep; + } + + private static String firstName(String fullName) { + if (fullName == null || fullName.isBlank()) return ""; + String[] parts = fullName.trim().split("\\s+"); + return parts[0]; + } + + private static String lastName(String fullName) { + if (fullName == null || fullName.isBlank()) return ""; + String[] parts = fullName.trim().split("\\s+"); + return parts.length > 1 ? parts[parts.length - 1] : parts[0]; + } +} diff --git a/src/main/java/com/signnow/samples/EmbeddedEditingAndSigningDG/IndexController.java b/src/main/java/com/signnow/samples/EmbeddedEditingAndSigningDG/IndexController.java index d8f388a..e3bf8ab 100644 --- a/src/main/java/com/signnow/samples/EmbeddedEditingAndSigningDG/IndexController.java +++ b/src/main/java/com/signnow/samples/EmbeddedEditingAndSigningDG/IndexController.java @@ -14,6 +14,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import com.signnow.api.document.response.DocumentDownloadGetResponse; @@ -198,7 +199,7 @@ private void updateDocumentGroupRecipients(ApiClient client, String documentGrou } private String createEmbeddedEditLink(ApiClient client, String documentGroupId) throws SignNowApiException { - String redirectUrl = "http://localhost:8080/samples/EmbeddedEditingAndSigningDG?page=page2-embedded-sending&document_group_id=" + documentGroupId; + String redirectUrl = AppConfig.sampleUrl("EmbeddedEditingAndSigningDG") + "?page=page2-embedded-sending&document_group_id=" + documentGroupId; var request = new com.signnow.api.embeddededitor.request.DocumentGroupEmbeddedEditorLinkPostRequest( redirectUrl, "self", 15 @@ -218,7 +219,7 @@ private String createEmbeddedInvite(ApiClient client, String documentGroupId, St String recipient1Email = findEmailByRoleName(recipientsResp, ROLE_RECIPIENT_1); String recipient2Email = findEmailByRoleName(recipientsResp, ROLE_RECIPIENT_2); - String redirectUrl = "http://localhost:8080/samples/EmbeddedEditingAndSigningDG?page=page4-status-download&document_group_id=" + documentGroupId; + String redirectUrl = AppConfig.sampleUrl("EmbeddedEditingAndSigningDG") + "?page=page4-status-download&document_group_id=" + documentGroupId; // -- Build invite list java.util.List inviteSteps = new LinkedList<>(); diff --git a/src/main/java/com/signnow/samples/EmbeddedSenderWithFormAndFirstSigner/IndexController.java b/src/main/java/com/signnow/samples/EmbeddedSenderWithFormAndFirstSigner/IndexController.java index 948fe3f..e1e5fdd 100644 --- a/src/main/java/com/signnow/samples/EmbeddedSenderWithFormAndFirstSigner/IndexController.java +++ b/src/main/java/com/signnow/samples/EmbeddedSenderWithFormAndFirstSigner/IndexController.java @@ -10,6 +10,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -39,10 +40,7 @@ @Controller public class IndexController implements ExampleInterface { - // Demo configuration - in production, these would come from environment/config private static final String DOCUMENT_GROUP_TEMPLATE_ID = "8e36720a436041ea837dc543ec00a3bc3559df45"; - private static final String USER_EMAIL = "example@example.com"; // Demo user - private static final String USER_PASSWORD = "example"; // Demo password // Demo field names that might exist in templates private static final String CUSTOMER_NAME_FIELD = "CustomerName"; @@ -52,8 +50,6 @@ public class IndexController implements ExampleInterface { private static final String PREPARE_CONTRACT_ROLE = "Prepare Contract"; private static final String CUSTOMER_SIGN_ROLE = "Customer to Sign"; - // Demo URL constants - private static final String REDIRECT_BASE_URL = "http://localhost:8080/samples/EmbeddedSenderWithFormAndFirstSigner"; private static final String SIGNING_URL_BASE = "https://app.signnow.com/webapp/documentgroup/signing"; @Override @@ -126,7 +122,7 @@ private ResponseEntity prepareDocumentGroup(Map data, ApiClie // Demo: Step 3 - Add recipients to Document Group with different emails for different roles String customerEmail = email; // Email from form for "Customer to Sign" - String preparerEmail = USER_EMAIL; // Email from config for "Prepare Contract" + String preparerEmail = AppConfig.demoUserEmail(); // Email from config for "Prepare Contract" Map addRecipientsResponse = updateDocumentGroupRecipients( client, documentGroupId, customerEmail, preparerEmail ); @@ -339,7 +335,7 @@ private String getEmailForRole(String recipientName, String customerEmail, Strin private Map createEmbeddedSendingUrl(ApiClient client, String documentGroupId) throws SignNowApiException { // Demo: Create embedded sending URL with redirect back to demo app - String redirectUrl = REDIRECT_BASE_URL + "?page=signing-page&document_group_id=" + documentGroupId; + String redirectUrl = AppConfig.sampleUrl("EmbeddedSenderWithFormAndFirstSigner") + "?page=signing-page&document_group_id=" + documentGroupId; var embeddedSendingRequest = new com.signnow.api.embeddedsending.request.DocumentGroupEmbeddedSendingLinkPostRequest( redirectUrl, // Demo: Redirect back to our demo app @@ -372,7 +368,7 @@ private Map createSigningLink(ApiClient client, String documentG String accessToken = (String) limitedTokenResponse.get("access_token"); // Demo: Build signing URL with parameters - redirect to status page after signing - String redirectUrl = REDIRECT_BASE_URL + "?page=status-page&document_group_id=" + documentGroupId; + String redirectUrl = AppConfig.sampleUrl("EmbeddedSenderWithFormAndFirstSigner") + "?page=status-page&document_group_id=" + documentGroupId; String signingUrl = SIGNING_URL_BASE + "?" + "document_group_id=" + documentGroupId + @@ -390,8 +386,8 @@ private Map createSigningLink(ApiClient client, String documentG private Map generateLimitedToken(ApiClient client, String documentGroupId) throws SignNowApiException { // Demo: Create token request with limited scope for document group var tokenRequest = new com.signnow.api.auth.request.TokenPostRequest( - USER_EMAIL, - USER_PASSWORD, + AppConfig.demoUserEmail(), + AppConfig.demoUserPassword(), "limited_signer_scope_token_for_document_group_invite/" + documentGroupId, "password", "" diff --git a/src/main/java/com/signnow/samples/EmbeddedSenderWithFormCreditLoanAgreement/IndexController.java b/src/main/java/com/signnow/samples/EmbeddedSenderWithFormCreditLoanAgreement/IndexController.java index 9f64ebb..dc2bf45 100644 --- a/src/main/java/com/signnow/samples/EmbeddedSenderWithFormCreditLoanAgreement/IndexController.java +++ b/src/main/java/com/signnow/samples/EmbeddedSenderWithFormCreditLoanAgreement/IndexController.java @@ -14,6 +14,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import com.signnow.api.document.response.DocumentDownloadGetResponse; @@ -98,7 +99,7 @@ private void prefillFields(ApiClient client, String documentId, String fullName) } private String getEmbeddedSendingLink(ApiClient client, String documentId) throws SignNowApiException { - String redirectUrl = "http://localhost:8080/samples/EmbeddedSenderWithFormCreditLoanAgreement?page=download-with-status&document_id=" + documentId; + String redirectUrl = AppConfig.sampleUrl("EmbeddedSenderWithFormCreditLoanAgreement") + "?page=download-with-status&document_id=" + documentId; DocumentEmbeddedSendingLinkPostRequest request = new DocumentEmbeddedSendingLinkPostRequest("invite", redirectUrl, 16, "self"); request.withDocumentId(documentId); diff --git a/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDG/IndexController.java b/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDG/IndexController.java index d3fab8b..50f014e 100644 --- a/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDG/IndexController.java +++ b/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDG/IndexController.java @@ -10,6 +10,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -39,19 +40,12 @@ @Controller public class IndexController implements ExampleInterface { - // Demo configuration - in production, these would come from environment/config private static final String DOCUMENT_GROUP_TEMPLATE_ID = "c8040bbc40804b89b63a0fa8c79b42a7ae4818c1"; - private static final String USER_EMAIL = "example@example.com"; // Demo user - - // Demo field names that might exist in templates // Demo recipient roles private static final String PREPARE_CONTRACT_ROLE = "Prepare Contract"; private static final String CUSTOMER_SIGN_ROLE = "Customer to Sign"; - // Demo URL constants - private static final String REDIRECT_BASE_URL = "http://localhost:8080/samples/EmbeddedSenderWithFormDG"; - @Override public ResponseEntity handleGet(Map queryParams) throws IOException { // Demo: Simple HTML page serving - use classpath resource @@ -121,7 +115,7 @@ private ResponseEntity prepareDocumentGroup(Map data, ApiClie // Demo: Step 3 - Add recipients to Document Group with different emails for different roles String customerEmail = email; // Email from form for "Customer to Sign" - String preparerEmail = USER_EMAIL; // Email from config for "Prepare Contract" + String preparerEmail = AppConfig.demoUserEmail(); // Email from config for "Prepare Contract" Map addRecipientsResponse = updateDocumentGroupRecipients( client, documentGroupId, customerEmail, preparerEmail ); @@ -324,7 +318,7 @@ private Map updateDocumentGroupRecipients( private Map createEmbeddedSendingUrl(ApiClient client, String documentGroupId) throws SignNowApiException { // Demo: Create embedded sending URL with redirect back to status page - String redirectUrl = REDIRECT_BASE_URL + "?" + + String redirectUrl = AppConfig.sampleUrl("EmbeddedSenderWithFormDG") + "?" + "page=status-page&document_group_id=" + documentGroupId; var embeddedSendingRequest = new com.signnow.api.embeddedsending.request.DocumentGroupEmbeddedSendingLinkPostRequest( diff --git a/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDGAdjunct/IndexController.java b/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDGAdjunct/IndexController.java index 7424ad6..108f171 100644 --- a/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDGAdjunct/IndexController.java +++ b/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDGAdjunct/IndexController.java @@ -10,6 +10,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -40,19 +41,12 @@ @Controller public class IndexController implements ExampleInterface { - // Demo configuration - in production, these would come from environment/config private static final String DOCUMENT_GROUP_TEMPLATE_ID = "e486ebf3fb814d55888bcbf75ceaf62b6db0de5a"; - private static final String USER_EMAIL = "example@example.com"; // Demo user - - // Demo field names that might exist in templates // Demo recipient roles private static final String PREPARE_CONTRACT_ROLE = "Prepare Contract"; private static final String CUSTOMER_SIGN_ROLE = "Customer to Sign"; - // Demo URL constants - private static final String REDIRECT_BASE_URL = "http://localhost:8080/samples/EmbeddedSenderWithFormDGAdjunct"; - @Override public ResponseEntity handleGet(Map queryParams) throws IOException { // Demo: Simple HTML page serving @@ -122,7 +116,7 @@ private ResponseEntity prepareDocumentGroup(Map data, ApiClie // Demo: Step 3 - Add recipients to Document Group with different emails for different roles String customerEmail = email; // Email from form for "Customer to Sign" - String preparerEmail = USER_EMAIL; // Email from config for "Prepare Contract" + String preparerEmail = AppConfig.demoUserEmail(); // Email from config for "Prepare Contract" Map addRecipientsResponse = updateDocumentGroupRecipients( client, documentGroupId, customerEmail, preparerEmail ); @@ -325,7 +319,7 @@ private Map updateDocumentGroupRecipients( private Map createEmbeddedSendingUrl(ApiClient client, String documentGroupId) throws SignNowApiException { // Demo: Create embedded sending URL with redirect back to status page - String redirectUrl = REDIRECT_BASE_URL + "?" + + String redirectUrl = AppConfig.sampleUrl("EmbeddedSenderWithFormDGAdjunct") + "?" + "page=status-page&document_group_id=" + documentGroupId; var embeddedSendingRequest = new com.signnow.api.embeddedsending.request.DocumentGroupEmbeddedSendingLinkPostRequest( diff --git a/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDGConstr/IndexController.java b/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDGConstr/IndexController.java index 8b5a413..2fd2202 100644 --- a/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDGConstr/IndexController.java +++ b/src/main/java/com/signnow/samples/EmbeddedSenderWithFormDGConstr/IndexController.java @@ -10,6 +10,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -40,19 +41,12 @@ @Controller public class IndexController implements ExampleInterface { - // Demo configuration - in production, these would come from environment/config private static final String DOCUMENT_GROUP_TEMPLATE_ID = "625ae557229d48d78728c281f46d3d8494f235a4"; - private static final String USER_EMAIL = "example@example.com"; // Demo user - - // Demo field names that might exist in templates // Demo recipient roles private static final String PREPARE_CONTRACT_ROLE = "Prepare Contract"; private static final String CUSTOMER_SIGN_ROLE = "Customer to Sign"; - // Demo URL constants - private static final String REDIRECT_BASE_URL = "http://localhost:8080/samples/EmbeddedSenderWithFormDGConstr"; - @Override public ResponseEntity handleGet(Map queryParams) throws IOException { // Demo: Simple HTML page serving @@ -122,7 +116,7 @@ private ResponseEntity prepareDocumentGroup(Map data, ApiClie // Demo: Step 3 - Add recipients to Document Group with different emails for different roles String customerEmail = email; // Email from form for "Customer to Sign" - String preparerEmail = USER_EMAIL; // Email from config for "Prepare Contract" + String preparerEmail = AppConfig.demoUserEmail(); // Email from config for "Prepare Contract" Map addRecipientsResponse = updateDocumentGroupRecipients( client, documentGroupId, customerEmail, preparerEmail ); @@ -325,7 +319,7 @@ private Map updateDocumentGroupRecipients( private Map createEmbeddedSendingUrl(ApiClient client, String documentGroupId) throws SignNowApiException { // Demo: Create embedded sending URL with redirect back to status page - String redirectUrl = REDIRECT_BASE_URL + "?" + + String redirectUrl = AppConfig.sampleUrl("EmbeddedSenderWithFormDGConstr") + "?" + "page=status-page&document_group_id=" + documentGroupId; var embeddedSendingRequest = new com.signnow.api.embeddedsending.request.DocumentGroupEmbeddedSendingLinkPostRequest( diff --git a/src/main/java/com/signnow/samples/EmbeddedSenderWithoutFormFile/IndexController.java b/src/main/java/com/signnow/samples/EmbeddedSenderWithoutFormFile/IndexController.java index 7de60e3..5f2293b 100644 --- a/src/main/java/com/signnow/samples/EmbeddedSenderWithoutFormFile/IndexController.java +++ b/src/main/java/com/signnow/samples/EmbeddedSenderWithoutFormFile/IndexController.java @@ -11,6 +11,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import com.signnow.api.document.response.DocumentDownloadGetResponse; @@ -76,7 +77,7 @@ private String getEmbeddedSendingLink(ApiClient client) throws SignNowApiExcepti CloneTemplatePostResponse cloneResponse = createDocumentFromTemplate(client); String documentId = cloneResponse.getId(); - String redirectUrl = "http://localhost:8080/samples/EmbeddedSenderWithoutFormFile?page=download-with-status&document_id=" + documentId; + String redirectUrl = AppConfig.sampleUrl("EmbeddedSenderWithoutFormFile") + "?page=download-with-status&document_id=" + documentId; DocumentEmbeddedSendingLinkPostRequest request = new DocumentEmbeddedSendingLinkPostRequest("document", redirectUrl, 16, "self"); request.withDocumentId(documentId); diff --git a/src/main/java/com/signnow/samples/EmbeddedSignerConsentForm/IndexController.java b/src/main/java/com/signnow/samples/EmbeddedSignerConsentForm/IndexController.java index 8119e9f..1d66276 100644 --- a/src/main/java/com/signnow/samples/EmbeddedSignerConsentForm/IndexController.java +++ b/src/main/java/com/signnow/samples/EmbeddedSignerConsentForm/IndexController.java @@ -17,6 +17,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -87,7 +88,7 @@ private String getEmbeddedInviteLink(ApiClient client, String documentId, String DocumentInviteLinkPostResponse embeddedInviteResponse = (DocumentInviteLinkPostResponse) client.send(embeddedInvite).getResponse(); - String redirectUrl = "http://localhost:8080/samples/EmbeddedSignerConsentForm?page=download-container&document_id=" + documentId; + String redirectUrl = AppConfig.sampleUrl("EmbeddedSignerConsentForm") + "?page=download-container&document_id=" + documentId; return embeddedInviteResponse.getData().getLink() + "&redirect_uri=" + java.net.URLEncoder.encode(redirectUrl, "UTF-8"); } diff --git a/src/main/java/com/signnow/samples/EmbeddedSignerConsumerServices/IndexController.java b/src/main/java/com/signnow/samples/EmbeddedSignerConsumerServices/IndexController.java index bd9a9d0..a5ad43c 100644 --- a/src/main/java/com/signnow/samples/EmbeddedSignerConsumerServices/IndexController.java +++ b/src/main/java/com/signnow/samples/EmbeddedSignerConsumerServices/IndexController.java @@ -19,6 +19,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -154,7 +155,7 @@ private String getEmbeddedInviteLink(ApiClient client, String documentId, String DocumentInviteLinkPostResponse embeddedInviteResponse = (DocumentInviteLinkPostResponse) client.send(embeddedInvite).getResponse(); - String redirectUrl = "http://localhost:8080/samples/EmbeddedSignerConsumerServices?page=finish&document_id=" + documentId; + String redirectUrl = AppConfig.sampleUrl("EmbeddedSignerConsumerServices") + "?page=finish&document_id=" + documentId; return embeddedInviteResponse.getData().getLink() + "&redirect_uri=" + java.net.URLEncoder.encode(redirectUrl, "UTF-8"); } diff --git a/src/main/java/com/signnow/samples/EmbeddedSignerPatientIntakeForm/IndexController.java b/src/main/java/com/signnow/samples/EmbeddedSignerPatientIntakeForm/IndexController.java index 7dea0b9..f84c31f 100644 --- a/src/main/java/com/signnow/samples/EmbeddedSignerPatientIntakeForm/IndexController.java +++ b/src/main/java/com/signnow/samples/EmbeddedSignerPatientIntakeForm/IndexController.java @@ -19,6 +19,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -160,7 +161,7 @@ private String getEmbeddedInviteLink(ApiClient client, String documentId, String DocumentInviteLinkPostResponse embeddedInviteResponse = (DocumentInviteLinkPostResponse) client.send(embeddedInvite).getResponse(); - String redirectUrl = "http://localhost:8080/samples/EmbeddedSignerPatientIntakeForm?page=download-container&document_id=" + documentId; + String redirectUrl = AppConfig.sampleUrl("EmbeddedSignerPatientIntakeForm") + "?page=download-container&document_id=" + documentId; return embeddedInviteResponse.getData().getLink() + "&redirect_uri=" + java.net.URLEncoder.encode(redirectUrl, "UTF-8"); } diff --git a/src/main/java/com/signnow/samples/EmbeddedSignerWithFormInsurance/IndexController.java b/src/main/java/com/signnow/samples/EmbeddedSignerWithFormInsurance/IndexController.java index d2c8753..ee3df4c 100644 --- a/src/main/java/com/signnow/samples/EmbeddedSignerWithFormInsurance/IndexController.java +++ b/src/main/java/com/signnow/samples/EmbeddedSignerWithFormInsurance/IndexController.java @@ -18,6 +18,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import com.signnow.api.document.response.DocumentDownloadGetResponse; @@ -108,7 +109,7 @@ private String getEmbeddedInviteLink(ApiClient client, String documentId, String DocumentInviteLinkPostResponse embeddedInviteResponse = (DocumentInviteLinkPostResponse) client.send(embeddedInvite).getResponse(); - String redirectUrl = "http://localhost:8080/samples/EmbeddedSignerWithFormInsurance?page=download-container&document_id=" + documentId; + String redirectUrl = AppConfig.sampleUrl("EmbeddedSignerWithFormInsurance") + "?page=download-container&document_id=" + documentId; return embeddedInviteResponse.getData().getLink() + "&redirect_uri=" + java.net.URLEncoder.encode(redirectUrl, "UTF-8"); } diff --git a/src/main/java/com/signnow/samples/HROnboardingSystem/IndexController.java b/src/main/java/com/signnow/samples/HROnboardingSystem/IndexController.java index 9c7da8a..f0505b0 100644 --- a/src/main/java/com/signnow/samples/HROnboardingSystem/IndexController.java +++ b/src/main/java/com/signnow/samples/HROnboardingSystem/IndexController.java @@ -29,6 +29,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -57,10 +58,6 @@ @Controller public class IndexController implements ExampleInterface { - // Demo configuration - in production, these would come from environment/config - private static final String USER_EMAIL = "example@example.com"; // Demo user - private static final String USER_PASSWORD = "example"; // Demo password - // Template IDs for HR onboarding documents private static final String I9_FORM_TEMPLATE_ID = "940989288b8b4c62a950b908333b5b21efd6a174"; private static final String NDA_TEMPLATE_ID = "a4f523d0cb234ffc99b0badc9e6f59111f76abc2"; @@ -72,9 +69,6 @@ public class IndexController implements ExampleInterface { private static final String TEXT_FIELD_156 = "Text Field 156"; private static final String EMAIL_FIELD = "Email"; - // Demo URL constants - private static final String REDIRECT_BASE_URL = "http://localhost:8080/samples/HROnboardingSystem"; - @Override public ResponseEntity handleGet(Map queryParams) throws IOException { // Demo: Simple HTML page serving - use classpath resource @@ -279,7 +273,7 @@ private Map sendInvite(ApiClient client, String documentGroupId, null, null, null, - REDIRECT_BASE_URL + "?page=status-page&document_group_id=" + documentGroupId, + AppConfig.sampleUrl("HROnboardingSystem") + "?page=status-page&document_group_id=" + documentGroupId, null, "self", null diff --git a/src/main/java/com/signnow/samples/ISVWithFormAndOneClickSendBasicPrefill/IndexController.java b/src/main/java/com/signnow/samples/ISVWithFormAndOneClickSendBasicPrefill/IndexController.java index 69d9623..d5ab9aa 100644 --- a/src/main/java/com/signnow/samples/ISVWithFormAndOneClickSendBasicPrefill/IndexController.java +++ b/src/main/java/com/signnow/samples/ISVWithFormAndOneClickSendBasicPrefill/IndexController.java @@ -10,6 +10,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -38,18 +39,12 @@ @Controller public class IndexController implements ExampleInterface { - // Demo configuration - in production, these would come from environment/config private static final String DOCUMENT_GROUP_TEMPLATE_ID = "6e79b9e6f9624984a7f054a7171d1644d0fb9934"; - private static final String USER_EMAIL = "example@example.com"; // Demo user - private static final String USER_PASSWORD = "example"; // Demo password // Demo field names that might exist in templates private static final String NAME_FIELD = "Name"; private static final String EMAIL_FIELD = "Email"; - // Demo URL constants - private static final String REDIRECT_BASE_URL = "http://localhost:8080/samples/ISVWithFormAndOneClickSendBasicPrefill"; - @Override public ResponseEntity handleGet(Map queryParams) throws IOException { // Demo: Simple HTML page serving @@ -267,7 +262,7 @@ private Map sendInvite(ApiClient client, String documentGroupId, null, null, null, - REDIRECT_BASE_URL + "?page=status-page&document_group_id=" + documentGroupId, // redirectUri + AppConfig.sampleUrl("ISVWithFormAndOneClickSendBasicPrefill") + "?page=status-page&document_group_id=" + documentGroupId, // redirectUri null, "self", // redirectTarget null diff --git a/src/main/java/com/signnow/samples/ISVWithFormAndOneClickSendMergeFields/IndexController.java b/src/main/java/com/signnow/samples/ISVWithFormAndOneClickSendMergeFields/IndexController.java index d6c06bc..e541ab7 100644 --- a/src/main/java/com/signnow/samples/ISVWithFormAndOneClickSendMergeFields/IndexController.java +++ b/src/main/java/com/signnow/samples/ISVWithFormAndOneClickSendMergeFields/IndexController.java @@ -12,6 +12,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -41,10 +42,7 @@ @Controller public class IndexController implements ExampleInterface { - // Demo configuration - in production, these would come from environment/config private static final String DOCUMENT_GROUP_TEMPLATE_ID = "8e36720a436041ea837dc543ec00a3bc3559df45"; - private static final String USER_EMAIL = "example@example.com"; // Demo user - private static final String USER_PASSWORD = "example"; // Demo password // Demo field names that might exist in templates private static final String CUSTOMER_NAME_FIELD = "CustomerName"; @@ -54,9 +52,6 @@ public class IndexController implements ExampleInterface { private static final String PREPARE_CONTRACT_ROLE = "Prepare Contract"; private static final String CUSTOMER_SIGN_ROLE = "Customer to Sign"; - // Demo URL constants - private static final String REDIRECT_BASE_URL = "http://localhost:8080/samples/ISVWithFormAndOneClickSendMergeFields"; - @Override public ResponseEntity handleGet(Map queryParams) throws IOException { // Demo: Simple HTML page serving - use classpath resource @@ -354,17 +349,6 @@ private Map sendInvite(ApiClient client, String documentGroupId, // Get recipients to use their roles var recipientsResponse = getDocumentGroupRecipients(client, documentGroupId); - - // Debug: Print recipients response as JSON - try { - ObjectMapper mapper = new ObjectMapper(); - String jsonResponse = mapper.writeValueAsString(recipientsResponse); - System.out.println("DEBUG: DocumentGroupRecipientsGetResponse JSON:"); - System.out.println(jsonResponse); - } catch (Exception e) { - System.out.println("DEBUG: Error serializing DocumentGroupRecipientsGetResponse: " + e.getMessage()); - } - var recipients = recipientsResponse.getData().getRecipients(); // Check if we have recipients @@ -386,7 +370,7 @@ private Map sendInvite(ApiClient client, String documentGroupId, emailToUse = email; // Use form email for "Prepare Contract" } else if (CUSTOMER_SIGN_ROLE.equals(recipient.getName())) { // Use config email for "Customer to Sign" - emailToUse = USER_EMAIL; + emailToUse = AppConfig.demoUserEmail(); } // Create invite email for this recipient @@ -411,7 +395,7 @@ private Map sendInvite(ApiClient client, String documentGroupId, null, null, null, - REDIRECT_BASE_URL + "?page=status-page&document_group_id=" + documentGroupId, // redirectUri + AppConfig.sampleUrl("ISVWithFormAndOneClickSendMergeFields") + "?page=status-page&document_group_id=" + documentGroupId, // redirectUri null, "self", // redirectTarget null diff --git a/src/main/java/com/signnow/samples/MedicalInsuranceClaimForm/IndexController.java b/src/main/java/com/signnow/samples/MedicalInsuranceClaimForm/IndexController.java index 3331810..87e8afa 100644 --- a/src/main/java/com/signnow/samples/MedicalInsuranceClaimForm/IndexController.java +++ b/src/main/java/com/signnow/samples/MedicalInsuranceClaimForm/IndexController.java @@ -21,6 +21,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -170,7 +171,7 @@ private String getEmbeddedInviteLink(ApiClient client, String documentId, String DocumentInviteLinkPostResponse embeddedInviteResponse = (DocumentInviteLinkPostResponse) client.send(embeddedInvite).getResponse(); - String redirectUrl = "http://localhost:8080/samples/MedicalInsuranceClaimForm?page=download-container&document_id=" + documentId; + String redirectUrl = AppConfig.sampleUrl("MedicalInsuranceClaimForm") + "?page=download-container&document_id=" + documentId; return embeddedInviteResponse.getData().getLink() + "&redirect_uri=" + java.net.URLEncoder.encode(redirectUrl, "UTF-8"); } diff --git a/src/main/java/com/signnow/samples/PrefillAndEmbeddedSendingAgreement/IndexController.java b/src/main/java/com/signnow/samples/PrefillAndEmbeddedSendingAgreement/IndexController.java index 5f72e3b..fed4802 100644 --- a/src/main/java/com/signnow/samples/PrefillAndEmbeddedSendingAgreement/IndexController.java +++ b/src/main/java/com/signnow/samples/PrefillAndEmbeddedSendingAgreement/IndexController.java @@ -14,6 +14,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import com.signnow.api.document.response.DocumentDownloadGetResponse; @@ -97,7 +98,7 @@ private void prefillFields(ApiClient client, String documentId, String name) thr } private String getEmbeddedSendingLink(ApiClient client, String documentId) throws SignNowApiException { - String redirectUrl = "http://localhost:8080/samples/PrefillAndEmbeddedSendingAgreement?page=download-with-status&document_id=" + documentId; + String redirectUrl = AppConfig.sampleUrl("PrefillAndEmbeddedSendingAgreement") + "?page=download-with-status&document_id=" + documentId; DocumentEmbeddedSendingLinkPostRequest request = new DocumentEmbeddedSendingLinkPostRequest("document", redirectUrl, 16, "self"); request.withDocumentId(documentId); diff --git a/src/main/java/com/signnow/samples/UploadEmbeddedEditingAndInvite/IndexController.java b/src/main/java/com/signnow/samples/UploadEmbeddedEditingAndInvite/IndexController.java new file mode 100644 index 0000000..7e1d742 --- /dev/null +++ b/src/main/java/com/signnow/samples/UploadEmbeddedEditingAndInvite/IndexController.java @@ -0,0 +1,203 @@ +package com.signnow.samples.UploadEmbeddedEditingAndInvite; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.signnow.Sdk; +import com.signnow.api.document.request.DocumentDownloadGetRequest; +import com.signnow.api.document.response.DocumentDownloadGetResponse; +import com.signnow.api.document.request.DocumentGetRequest; +import com.signnow.api.document.response.DocumentGetResponse; +import com.signnow.api.document.request.DocumentPostRequest; +import com.signnow.api.document.response.DocumentPostResponse; +import com.signnow.api.documentinvite.request.SendInvitePostRequest; +import com.signnow.api.documentinvite.request.data.To; +import com.signnow.api.documentinvite.request.data.ToCollection; +import com.signnow.api.embeddededitor.request.DocumentEmbeddedEditorLinkPostRequest; +import com.signnow.api.embeddededitor.response.DocumentEmbeddedEditorLinkPostResponse; +import com.signnow.core.ApiClient; +import com.signnow.core.exception.SignNowApiException; +import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +@Controller +public class IndexController implements ExampleInterface { + + private static final String SAMPLE_NAME = "UploadEmbeddedEditingAndInvite"; + private static final String SENDER_EMAIL = "sender@signnow.com"; + private static final String PDF_PATH = + "src/main/resources/static/samples/UploadEmbeddedSender/Sales Proposal.pdf"; + + @Override + public ResponseEntity handleGet(Map queryParams) throws IOException { + try (var inputStream = getClass().getResourceAsStream("/static/samples/" + SAMPLE_NAME + "/index.html")) { + if (inputStream == null) { + throw new IOException("HTML file not found in classpath"); + } + String html = new String(inputStream.readAllBytes()); + return ResponseEntity.ok().header("Content-Type", "text/html").body(html); + } + } + + @Override + public ResponseEntity handlePost(String formData) throws IOException, SignNowApiException { + Map data = new ObjectMapper().readValue(formData, Map.class); + String action = (String) data.getOrDefault("action", ""); + + ApiClient client = new Sdk().build().authenticate().getApiClient(); + + switch (action) { + case "upload_and_create_dg": + return uploadDocument(client); + case "create_embedded_edit": + return createEmbeddedEdit(client, data); + case "create_invite": + return createInvite(client, data); + case "invite-status": + return inviteStatus(client, data); + case "download-document": + return downloadDocument(client, data); + default: + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", "Invalid action"); + return ResponseEntity.badRequest().body(new ObjectMapper().writeValueAsString(error)); + } + } + + private ResponseEntity uploadDocument(ApiClient client) throws SignNowApiException, IOException { + if (!Files.exists(Paths.get(PDF_PATH))) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", "PDF file not found: " + PDF_PATH); + return ResponseEntity.status(500).body(new ObjectMapper().writeValueAsString(error)); + } + + DocumentPostRequest req = new DocumentPostRequest(new File(PDF_PATH), "Sales Proposal"); + DocumentPostResponse resp = (DocumentPostResponse) client.send(req).getResponse(); + + Map result = new HashMap<>(); + result.put("success", true); + result.put("message", "Document uploaded successfully"); + result.put("document_id", resp.getId()); + return ResponseEntity.ok().header("Content-Type", "application/json") + .body(new ObjectMapper().writeValueAsString(result)); + } + + private ResponseEntity createEmbeddedEdit(ApiClient client, Map data) + throws SignNowApiException, IOException { + String documentId = (String) data.get("document_id"); + if (documentId == null || documentId.isBlank()) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", "Document ID is required"); + return ResponseEntity.badRequest().body(new ObjectMapper().writeValueAsString(error)); + } + + String redirectUrl = AppConfig.sampleUrl(SAMPLE_NAME) + "?page=invite-page&document_id=" + documentId; + + DocumentEmbeddedEditorLinkPostRequest req = + new DocumentEmbeddedEditorLinkPostRequest(redirectUrl, "self", 15); + req.withDocumentId(documentId); + DocumentEmbeddedEditorLinkPostResponse resp = + (DocumentEmbeddedEditorLinkPostResponse) client.send(req).getResponse(); + + Map result = new HashMap<>(); + result.put("success", true); + result.put("edit_link", resp.getData().getUrl()); + return ResponseEntity.ok().header("Content-Type", "application/json") + .body(new ObjectMapper().writeValueAsString(result)); + } + + private ResponseEntity createInvite(ApiClient client, Map data) + throws SignNowApiException, IOException { + String documentId = (String) data.get("document_id"); + String signerEmail = (String) data.getOrDefault("signer_email", ""); + String signerName = (String) data.getOrDefault("signer_name", ""); + + if (documentId == null || signerEmail.isBlank() || signerName.isBlank()) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", "Document ID, signer email and name are required"); + return ResponseEntity.badRequest().body(new ObjectMapper().writeValueAsString(error)); + } + + DocumentGetRequest docReq = new DocumentGetRequest(); + docReq.withDocumentId(documentId); + DocumentGetResponse docResp = (DocumentGetResponse) client.send(docReq).getResponse(); + + String redirectUri = AppConfig.sampleUrl(SAMPLE_NAME) + "?page=status-page&document_id=" + documentId; + + ToCollection toCollection = new ToCollection(); + for (var role : docResp.getRoles()) { + toCollection.add(new To( + signerEmail, + role.getUniqueId(), + role.getName(), + 1, + "Document Signing Request - Action Required", + "Dear " + signerName + ", please review and sign the uploaded document." + )); + } + + SendInvitePostRequest inviteReq = new SendInvitePostRequest( + documentId, toCollection, SENDER_EMAIL, + "Document Signing Request - Action Required", + "Dear " + signerName + ", please review and sign the uploaded document." + ); + client.send(inviteReq); + + Map result = new HashMap<>(); + result.put("success", true); + result.put("message", "Invite sent successfully"); + return ResponseEntity.ok().header("Content-Type", "application/json") + .body(new ObjectMapper().writeValueAsString(result)); + } + + private ResponseEntity inviteStatus(ApiClient client, Map data) + throws SignNowApiException, IOException { + String documentId = (String) data.get("document_id"); + + DocumentGetRequest req = new DocumentGetRequest(); + req.withDocumentId(documentId); + DocumentGetResponse resp = (DocumentGetResponse) client.send(req).getResponse(); + + List> statuses = new LinkedList<>(); + for (var invite : resp.getFieldInvites()) { + Map entry = new HashMap<>(); + entry.put("name", invite.getEmail()); + entry.put("status", invite.getStatus()); + entry.put("timestamp", ""); + statuses.add(entry); + } + return ResponseEntity.ok().header("Content-Type", "application/json") + .body(new ObjectMapper().writeValueAsString(statuses)); + } + + private ResponseEntity downloadDocument(ApiClient client, Map data) + throws SignNowApiException, IOException { + String documentId = (String) data.get("document_id"); + + DocumentDownloadGetRequest req = new DocumentDownloadGetRequest(); + req.withDocumentId(documentId).withType("collapsed"); + DocumentDownloadGetResponse resp = (DocumentDownloadGetResponse) client.send(req).getResponse(); + + File file = resp.getFile(); + byte[] content = Files.readAllBytes(file.toPath()); + file.delete(); + + return ResponseEntity.ok() + .header("Content-Type", "application/pdf") + .header("Content-Disposition", "attachment; filename=\"final_document.pdf\"") + .body(content); + } +} diff --git a/src/main/java/com/signnow/samples/UploadEmbeddedSender/IndexController.java b/src/main/java/com/signnow/samples/UploadEmbeddedSender/IndexController.java index 42ee12c..3c3ad8c 100644 --- a/src/main/java/com/signnow/samples/UploadEmbeddedSender/IndexController.java +++ b/src/main/java/com/signnow/samples/UploadEmbeddedSender/IndexController.java @@ -20,6 +20,7 @@ import com.signnow.core.ApiClient; import com.signnow.core.exception.SignNowApiException; import com.signnow.javasampleapp.ExampleInterface; +import com.signnow.javasampleapp.config.AppConfig; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -143,7 +144,7 @@ private Map createDocumentGroup(ApiClient client, String documen private Map createEmbeddedSendingUrl(ApiClient client, String documentGroupId) throws SignNowApiException { // Return URL after embedded sending to redirect to status page - String redirectUrl = "http://localhost:8080/samples/UploadEmbeddedSender?" + + String redirectUrl = AppConfig.sampleUrl("UploadEmbeddedSender") + "?" + "page=status-page&document_group_id=" + documentGroupId; DocumentGroupEmbeddedSendingLinkPostRequest embeddedSendingRequest = new DocumentGroupEmbeddedSendingLinkPostRequest( diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0379837..a13a71e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,17 @@ spring.application.name=java-sample-app +server.port=${PORT:8080} server.error.whitelabel.enabled=false -signnow.api.signer_email=test@example.com + +# App configuration +app.base-url=${APP_BASE_URL:http://localhost:8080} +signnow.api.signer_email=${SIGNNOW_SIGNER_EMAIL:test@example.com} +signnow.api.demo_user_email=${SIGNNOW_DEMO_USER_EMAIL:example@example.com} +signnow.api.demo_user_password=${SIGNNOW_DEMO_USER_PASSWORD:example} + +# Actuator - restrict to health/info only +management.endpoints.web.exposure.include=health,info +management.endpoint.health.show-details=never + +# Logging +logging.level.com.signnow=INFO +logging.pattern.level=%5p [%X{traceId:-},%X{spanId:-}] diff --git a/src/main/resources/static/samples/EVDemoSendingAnd3EmbeddedSigners/index.html b/src/main/resources/static/samples/EVDemoSendingAnd3EmbeddedSigners/index.html new file mode 100644 index 0000000..61c66fd --- /dev/null +++ b/src/main/resources/static/samples/EVDemoSendingAnd3EmbeddedSigners/index.html @@ -0,0 +1,265 @@ + + + + + + + EVDemoSendingAnd3EmbeddedSigners + + + + + + + + + + + + +
+ SignNow Logo +
+ + + + + + + + + + + + + + + diff --git a/src/main/resources/static/samples/EmbeddedSenderWithFormAndFirstSigner/index.html b/src/main/resources/static/samples/EmbeddedSenderWithFormAndFirstSigner/index.html index 8755d97..b30f0b7 100644 --- a/src/main/resources/static/samples/EmbeddedSenderWithFormAndFirstSigner/index.html +++ b/src/main/resources/static/samples/EmbeddedSenderWithFormAndFirstSigner/index.html @@ -166,18 +166,16 @@

No signers found

// Redirect to signing URL window.location.href = result.signing_url; } else { - signingContent.innerHTML = ` -
- Error: ${result.message || 'Failed to create signing URL'} -
- `; + const errorDiv = document.createElement('div'); + errorDiv.className = 'alert alert-danger'; + errorDiv.textContent = 'Error: ' + (result.message || 'Failed to create signing URL'); + signingContent.replaceChildren(errorDiv); } } catch (error) { - signingContent.innerHTML = ` -
- Error: ${error.message} -
- `; + const errorDiv = document.createElement('div'); + errorDiv.className = 'alert alert-danger'; + errorDiv.textContent = 'Error: ' + error.message; + signingContent.replaceChildren(errorDiv); } } diff --git a/src/main/resources/static/samples/UploadEmbeddedEditingAndInvite/index.html b/src/main/resources/static/samples/UploadEmbeddedEditingAndInvite/index.html new file mode 100644 index 0000000..69a201a --- /dev/null +++ b/src/main/resources/static/samples/UploadEmbeddedEditingAndInvite/index.html @@ -0,0 +1,300 @@ + + + + + + Upload Embedded Editing and Invite - SignNow Sample + + + + + +
+ SignNow Logo +
+ + +
+

Upload Document for Editing and Signing

+

This sample uploads a pre-loaded PDF to SignNow, creates an embedded editing link, and sends an invite for signing.

+ + + + + +
+ + + + + + + + + + + + diff --git a/src/main/resources/static/samples/UploadEmbeddedSender/index.html b/src/main/resources/static/samples/UploadEmbeddedSender/index.html index ec95746..39ddb91 100644 --- a/src/main/resources/static/samples/UploadEmbeddedSender/index.html +++ b/src/main/resources/static/samples/UploadEmbeddedSender/index.html @@ -91,18 +91,16 @@

No signers found

// Redirect to embedded sending URL window.location.href = result.embedded_url; } else { - uploadContent.innerHTML = ` -
- Error: ${result.message || 'Failed to upload document and create embedded sending link'} -
- `; + const errorDiv = document.createElement('div'); + errorDiv.className = 'alert alert-danger'; + errorDiv.textContent = 'Error: ' + (result.message || 'Failed to upload document and create embedded sending link'); + uploadContent.replaceChildren(errorDiv); } } catch (error) { - uploadContent.innerHTML = ` -
- Error: ${error.message} -
- `; + const errorDiv = document.createElement('div'); + errorDiv.className = 'alert alert-danger'; + errorDiv.textContent = 'Error: ' + error.message; + uploadContent.replaceChildren(errorDiv); console.error(error); } } diff --git a/src/main/resources/views/index.html b/src/main/resources/views/index.html index c676ca0..d762440 100644 --- a/src/main/resources/views/index.html +++ b/src/main/resources/views/index.html @@ -2,7 +2,7 @@ - + Sample Apps diff --git a/src/test/java/com/signnow/javasampleapp/controllers/RoutingControllerTest.java b/src/test/java/com/signnow/javasampleapp/controllers/RoutingControllerTest.java new file mode 100644 index 0000000..eb4efa8 --- /dev/null +++ b/src/test/java/com/signnow/javasampleapp/controllers/RoutingControllerTest.java @@ -0,0 +1,51 @@ +package com.signnow.javasampleapp.controllers; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = {RoutingController.class, GlobalExceptionHandler.class}) +class RoutingControllerTest { + + @Autowired + MockMvc mvc; + + @Test + void root_returns404ErrorPage() throws Exception { + mvc.perform(get("/")) + .andExpect(status().isNotFound()) + .andExpect(content().contentTypeCompatibleWith("text/html")); + } + + @Test + void getExample_withInvalidName_returns404() throws Exception { + mvc.perform(get("/samples/has-dash")) + .andExpect(status().isNotFound()); + } + + @Test + void getExample_withPathTraversal_returns404() throws Exception { + mvc.perform(get("/samples/..%2Fevil")) + .andExpect(status().isNotFound()); + } + + @Test + void getExample_withUnknownButValidName_returns404() throws Exception { + mvc.perform(get("/samples/NoSuchSample_xyz")) + .andExpect(status().isNotFound()); + } + + @Test + void postExample_withInvalidName_returns404() throws Exception { + mvc.perform(post("/api/samples/bad-name") + .contentType("application/json") + .content("{}")) + .andExpect(status().isNotFound()); + } +}