diff --git a/.github/workflows/deploy-proxy-server.yml b/.github/workflows/deploy-proxy-server.yml new file mode 100644 index 0000000..da8c5ca --- /dev/null +++ b/.github/workflows/deploy-proxy-server.yml @@ -0,0 +1,70 @@ +name: Deploy Proxy Server + +on: + workflow_dispatch: + +env: + PROJECT_ID: ${{ vars.GCP_PROJECT_ID }} + REGION: ${{ vars.GCP_REGION }} + SERVICE_NAME: ${{ vars.GCP_SERVICE_NAME }} + SUPERDOC_SERVICES_API_KEY: ${{ secrets.SUPERDOC_SERVICES_API_KEY }} + SUPERDOC_SERVICES_BASE_URL: ${{ vars.SUPERDOC_SERVICES_BASE_URL }} + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Auth to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@v2 + with: + project_id: ${{ env.PROJECT_ID }} + + - name: Build and push container with Cloud Build + run: | + REGION="${REGION:-us-central1}" + SERVICE_NAME="${SERVICE_NAME:-esign-demo-proxy-server}" + IMAGE="gcr.io/${PROJECT_ID}/${SERVICE_NAME}:${GITHUB_SHA}" + + echo "REGION=${REGION}" >> $GITHUB_ENV + echo "SERVICE_NAME=${SERVICE_NAME}" >> $GITHUB_ENV + echo "IMAGE=${IMAGE}" >> $GITHUB_ENV + + gcloud builds submit demo/server --tag "${IMAGE}" + env: + PROJECT_ID: ${{ env.PROJECT_ID }} + REGION: ${{ env.REGION }} + SERVICE_NAME: ${{ env.SERVICE_NAME }} + + - name: Deploy container to Cloud Run + run: | + REGION="${REGION:-us-central1}" + SERVICE_NAME="${SERVICE_NAME:-esign-demo-proxy-server}" + IMAGE="${IMAGE}" + SUPERDOC_SERVICES_BASE_URL="${SUPERDOC_SERVICES_BASE_URL:-https://api.superdoc.dev}" + + gcloud run deploy "${SERVICE_NAME}" \ + --image "${IMAGE}" \ + --region "${REGION}" \ + --memory=1Gi \ + --cpu=1 \ + --allow-unauthenticated \ + --set-env-vars SUPERDOC_SERVICES_BASE_URL="${SUPERDOC_SERVICES_BASE_URL}" \ + --set-secrets="SUPERDOC_SERVICES_API_KEY=esign-demo-sd-services-api-key:latest" + env: + IMAGE: ${{ env.IMAGE }} + REGION: ${{ env.REGION }} + SERVICE_NAME: ${{ env.SERVICE_NAME }} + SUPERDOC_SERVICES_API_KEY: ${{ env.SUPERDOC_SERVICES_API_KEY }} + SUPERDOC_SERVICES_BASE_URL: ${{ env.SUPERDOC_SERVICES_BASE_URL }} diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..bf1d474 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,51 @@ +# Demo + +This demo shows how to integrate `@superdoc-dev/esign` into a React application. The frontend sends signing and download requests to a proxy server, which securely communicates with the SuperDoc Services API. + +## Prerequisites + +You'll need a SuperDoc Services API key. [Get your API key here](https://docs.superdoc.dev/api-reference/authentication/register). + +## Setup + +1. Build the main package (from repo root): + ```bash + pnpm build + ``` + +2. Install dependencies: + ```bash + cd demo + pnpm install + cd server + pnpm install + ``` + +3. Create `.env` file in `demo/server/`: + ``` + SUPERDOC_SERVICES_API_KEY=your_key_here + ``` + +4. Update `demo/vite.config.ts` to proxy to localhost: + ```ts + proxy: { + '/v1': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + }, + ``` + +## Running + +Start the proxy server: +```bash +cd demo/server +pnpm start +``` + +In a separate terminal, start the frontend: +```bash +cd demo +pnpm dev +``` diff --git a/demo/package.json b/demo/package.json index a09ce3c..a244d2c 100644 --- a/demo/package.json +++ b/demo/package.json @@ -11,6 +11,7 @@ "@superdoc-dev/esign": "link:../.", "react": "^18.3.1", "react-dom": "^18.3.1", + "signature_pad": "^5.1.1", "superdoc": "^0.35.3" }, "devDependencies": { diff --git a/demo/pnpm-lock.yaml b/demo/pnpm-lock.yaml index 4caf216..08dd888 100644 --- a/demo/pnpm-lock.yaml +++ b/demo/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + signature_pad: + specifier: ^5.1.1 + version: 5.1.3 superdoc: specifier: ^0.35.3 version: 0.35.3(@hocuspocus/provider@2.15.3(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19))(canvas@2.11.2)(pdfjs-dist@4.6.82)(typescript@5.9.3)(y-prosemirror@1.3.7(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.2)(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19))(yjs@13.6.19) @@ -1218,6 +1221,9 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signature_pad@5.1.3: + resolution: {integrity: sha512-zyxW5vuJVnQdGcU+kAj9FYl7WaAunY3kA5S7mPg0xJiujL9+sPAWfSQHS5tXaJXDUa4FuZeKhfdCDQ6K3wfkpQ==} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -2681,6 +2687,8 @@ snapshots: signal-exit@3.0.7: optional: true + signature_pad@5.1.3: {} + simple-concat@1.0.1: optional: true diff --git a/demo/server/.dockerignore b/demo/server/.dockerignore new file mode 100644 index 0000000..dbb23e6 --- /dev/null +++ b/demo/server/.dockerignore @@ -0,0 +1,6 @@ +node_modules +npm-debug.log +.env +.env.* +Dockerfile +.dockerignore diff --git a/demo/server/.env.example b/demo/server/.env.example new file mode 100644 index 0000000..a65e3b7 --- /dev/null +++ b/demo/server/.env.example @@ -0,0 +1,3 @@ +PORT=3003 +SUPERDOC_SERVICES_API_KEY=replace-with-your-superdoc-api-key +SUPERDOC_SERVICES_BASE_URL=https://api.superdoc.dev diff --git a/demo/server/Dockerfile b/demo/server/Dockerfile new file mode 100644 index 0000000..aa7d575 --- /dev/null +++ b/demo/server/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json ./ +RUN corepack enable && pnpm install --prod --no-lockfile + +COPY . . + +# Cloud Run/Functions set PORT; default to 8080 +ENV PORT=8080 + +CMD ["pnpm", "start"] diff --git a/demo/server/package.json b/demo/server/package.json new file mode 100644 index 0000000..962a673 --- /dev/null +++ b/demo/server/package.json @@ -0,0 +1,15 @@ +{ + "name": "esign-proxy-server", + "private": true, + "type": "module", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2" + } +} diff --git a/demo/server/pnpm-lock.yaml b/demo/server/pnpm-lock.yaml new file mode 100644 index 0000000..961b1be --- /dev/null +++ b/demo/server/pnpm-lock.yaml @@ -0,0 +1,606 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + cors: + specifier: ^2.8.5 + version: 2.8.5 + dotenv: + specifier: ^16.4.5 + version: 16.6.1 + express: + specifier: ^4.19.2 + version: 4.22.1 + +packages: + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + +snapshots: + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + array-flatten@1.1.1: {} + + body-parser@1.20.4: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + depd@2.0.0: {} + + destroy@1.2.0: {} + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + negotiator@0.6.3: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + parseurl@1.3.3: {} + + path-to-regexp@0.1.12: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.2: {} + + toidentifier@1.0.1: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + unpipe@1.0.0: {} + + utils-merge@1.0.1: {} + + vary@1.1.2: {} diff --git a/demo/server/server.js b/demo/server/server.js new file mode 100644 index 0000000..36d5b3c --- /dev/null +++ b/demo/server/server.js @@ -0,0 +1,212 @@ +import express from 'express'; +import cors from 'cors'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const app = express(); +const PORT = process.env.PORT || 3001; +const SUPERDOC_SERVICES_API_KEY = process.env.SUPERDOC_SERVICES_API_KEY; +const SUPERDOC_SERVICES_BASE_URL = + process.env.SUPERDOC_SERVICES_BASE_URL || 'https://api.superdoc.dev'; +const CONSENT_FIELD_IDS = new Set(['consent_agreement', 'terms', 'email', '406948812']); +const SIGNATURE_FIELD_ID = '789012'; +const IP_ADDRESS = '127.0.0.1'; // Replace with real client IP once available +const DEMO_USER = { + name: 'Demo User', + email: 'demo@superdoc.dev', + userAgent: 'demo-user-agent', +}; + +app.use( + cors({ + origin: 'https://esign.superdoc.dev', + }), +); +app.use(express.json({ limit: '50mb' })); + +app.get('/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +const normalizeFields = (fieldsPayload = {}) => { + const documentFields = Array.isArray(fieldsPayload.document) ? fieldsPayload.document : []; + const signerFields = Array.isArray(fieldsPayload.signer) ? fieldsPayload.signer : []; + + return [...documentFields, ...signerFields] + .filter((field) => field?.id && !CONSENT_FIELD_IDS.has(field.id)) + .map((field) => { + const isSignatureField = field.id === SIGNATURE_FIELD_ID; + const value = field.value ?? ''; + const isDrawnSignature = typeof value === 'string' && value.startsWith('data:image/'); + const type = isSignatureField && isDrawnSignature ? 'signature' : 'text'; + + const normalized = { id: field.id, value, type }; + if (type === 'signature') { + normalized.options = { + bottomLabel: { text: `ip: ${IP_ADDRESS}`, color: '#666' }, + }; + } + return normalized; + }); +}; + +const annotateDocument = async ({ documentUrl, fields }) => { + const response = await fetch(`${SUPERDOC_SERVICES_BASE_URL}/v1/annotate?to=pdf`, { + method: 'POST', + headers: { + Authorization: `Bearer ${SUPERDOC_SERVICES_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + document: { url: documentUrl }, + fields: fields || [], + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(error || 'Failed to annotate document'); + } + + const data = await response.json(); + return { + base64: data?.document?.base64, + contentType: data?.document?.contentType || 'application/pdf', + }; +}; + +const sendPdfBuffer = (res, base64, fileName, contentType = 'application/pdf') => { + const buffer = Buffer.from(base64, 'base64'); + res.setHeader('Content-Type', contentType); + res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`); + res.send(buffer); +}; + +app.post('/v1/download', async (req, res) => { + try { + const { document, fields = {}, fileName = 'document.pdf' } = req.body || {}; + + if (!SUPERDOC_SERVICES_API_KEY) { + return res.status(500).json({ error: 'Missing SUPERDOC_SERVICES_API_KEY on the server' }); + } + + if (!document?.url) { + return res.status(400).json({ error: 'document.url is required' }); + } + + const annotatedFields = normalizeFields(fields); + + const { base64, contentType } = await annotateDocument({ + documentUrl: document.url, + fields: annotatedFields, + }); + + if (!base64) { + return res.status(502).json({ + error: 'Annotate response missing PDF content', + }); + } + + sendPdfBuffer(res, base64, fileName || 'document.pdf', contentType); + } catch (error) { + console.error('Error processing download:', error); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +app.post('/v1/sign', async (req, res) => { + try { + const { + document, + documentFields = [], + signerFields = [], + auditTrail = [], + eventId, + certificate, + metadata, + fileName = 'signed-document.pdf', + } = req.body || {}; + + if (!SUPERDOC_SERVICES_API_KEY) { + return res.status(500).json({ error: 'Missing SUPERDOC_SERVICES_API_KEY on the server' }); + } + + if (!document?.url) { + return res.status(400).json({ error: 'document.url is required' }); + } + + const annotatedFields = normalizeFields({ + document: documentFields, + signer: signerFields, + }); + + const { base64: annotatedBase64 } = await annotateDocument({ + documentUrl: document.url, + fields: annotatedFields, + }); + + if (!annotatedBase64) { + return res.status(502).json({ + error: 'Annotate response missing document content', + }); + } + + const signPayload = { + eventId, + document: { base64: annotatedBase64 }, + auditTrail, + signer: { + name: DEMO_USER.name, + email: DEMO_USER.email, + ip: IP_ADDRESS, + userAgent: DEMO_USER.userAgent, + }, + certificate, + metadata, + }; + + const signResponse = await fetch(`${SUPERDOC_SERVICES_BASE_URL}/v1/sign`, { + method: 'POST', + headers: { + Authorization: `Bearer ${SUPERDOC_SERVICES_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(signPayload), + }); + + if (!signResponse.ok) { + const error = await signResponse.text(); + console.error('SuperDoc sign error:', error); + return res.status(signResponse.status).json({ + error: 'Failed to sign document', + details: error, + }); + } + + const signData = await signResponse.json(); + const signedBase64 = signData?.document?.base64; + const contentType = signData?.document?.contentType || 'application/pdf'; + + if (!signedBase64) { + return res.status(502).json({ + error: 'Sign response missing document content', + }); + } + + sendPdfBuffer(res, signedBase64, fileName, contentType); + } catch (error) { + console.error('Error signing document:', error); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +app.listen(PORT, () => { + console.log(`Proxy server running on http://localhost:${PORT}`); +}); diff --git a/demo/src/App.tsx b/demo/src/App.tsx index cd6d844..219d08f 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -1,14 +1,85 @@ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import SuperDocESign from '@superdoc-dev/esign'; -import type { SubmitData, SigningState, FieldChange, DownloadData } from '@superdoc-dev/esign'; +import type { + SubmitData, + SigningState, + FieldChange, + DownloadData, + SuperDocESignHandle, +} from '@superdoc-dev/esign'; +import CustomSignature from './CustomSignature'; import 'superdoc/style.css'; import './App.css'; +const documentSource = + 'https://storage.googleapis.com/public_static_hosting/public_demo_docs/service_agreement_updated.docx'; + +// Helper to download a response blob as a file +const downloadBlob = async (response: Response, fileName: string) => { + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + a.click(); + URL.revokeObjectURL(url); +}; + +// Document field definitions with labels +const documentFieldsConfig = [ + { + id: '123456', + label: 'Date', + defaultValue: new Date().toLocaleDateString(), + readOnly: true, + type: 'text' as const, + }, + { + id: '234567', + label: 'Full Name', + defaultValue: 'John Doe', + readOnly: false, + type: 'text' as const, + }, + { + id: '345678', + label: 'Company', + defaultValue: 'SuperDoc', + readOnly: false, + type: 'text' as const, + }, + { id: '456789', label: 'Plan', defaultValue: 'Premium', readOnly: false, type: 'text' as const }, + { id: '567890', label: 'State', defaultValue: 'CA', readOnly: false, type: 'text' as const }, + { + id: '678901', + label: 'Address', + defaultValue: '123 Main St, Anytown, USA', + readOnly: false, + type: 'text' as const, + }, +]; + export function App() { const [submitted, setSubmitted] = useState(false); const [submitData, setSubmitData] = useState(null); const [events, setEvents] = useState([]); + // Stable eventId that persists across renders + const [eventId] = useState(() => `demo-${Date.now()}`); + + // Ref to the esign component + const esignRef = useRef(null); + + // State for document field values + const [documentFields, setDocumentFields] = useState>(() => + Object.fromEntries(documentFieldsConfig.map((f) => [f.id, f.defaultValue])), + ); + + const updateDocumentField = (id: string, value: string) => { + setDocumentFields((prev) => ({ ...prev, [id]: value })); + esignRef.current?.updateFieldInDocument({ id, value }); + }; + const log = (msg: string) => { const time = new Date().toLocaleTimeString(); console.log(`[${time}] ${msg}`); @@ -16,27 +87,72 @@ export function App() { }; const handleSubmit = async (data: SubmitData) => { - log('✓ Agreement signed'); + log('⏳ Signing document...'); console.log('Submit data:', data); - setSubmitted(true); - setSubmitData(data); + + try { + const response = await fetch('/v1/sign', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + document: { url: documentSource }, + documentFields: data.documentFields, + signerFields: data.signerFields, + auditTrail: data.auditTrail, + eventId: data.eventId, + certificate: { enable: true }, + metadata: { + company: documentFields['345678'], + plan: documentFields['456789'], + }, + fileName: `signed_agreement_${data.eventId}.pdf`, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(error || 'Failed to sign document'); + } + + await downloadBlob(response, `signed_agreement_${data.eventId}.pdf`); + + log('✓ Document signed and downloaded!'); + setSubmitted(true); + setSubmitData(data); + } catch (error) { + console.error('Error signing document:', error); + log(`✗ Signing failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } }; const handleDownload = async (data: DownloadData) => { - // Send to your backend for DOCX to PDF conversion - const response = await fetch('/v1/convert-to-pdf', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }); - - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = data.fileName; - a.click(); - URL.revokeObjectURL(url); + try { + if (typeof data.documentSource !== 'string') { + log('Download requires a document URL.'); + return; + } + + const response = await fetch('/v1/download', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + document: { url: data.documentSource }, + fields: data.fields, + fileName: data.fileName, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(error || 'Failed to annotate document'); + } + + await downloadBlob(response, data.fileName || 'document.pdf'); + log('✓ Downloaded PDF'); + } catch (error) { + console.error('Error processing document:', error); + log('Download failed'); + } }; const handleStateChange = (state: SigningState) => { @@ -50,12 +166,16 @@ export function App() { }; const handleFieldChange = (field: FieldChange) => { - log(`Field "${field.id}": ${field.value}`); + const displayValue = + typeof field.value === 'string' && field.value.startsWith('data:image/') + ? `${field.value.slice(0, 30)}... (base64 image)` + : field.value; + log(`Field "${field.id}": ${displayValue}`); console.log('Field change:', field); }; return ( -
+

Use the document toolbar to download the current agreement at any time.

- - - {/* Event Log */} - {events.length > 0 && ( + +
+ {/* Main content */} +
+ ({ + id: f.id, + value: documentFields[f.id], + type: f.type, + })), + signer: [ + { + id: '789012', + type: 'signature', + label: 'Your Signature', + validation: { required: true }, + component: CustomSignature, + }, + { + id: 'terms', + type: 'checkbox', + label: 'I accept the terms and conditions', + validation: { required: true }, + }, + { + id: 'email', + type: 'checkbox', + label: 'Send me a copy of the agreement', + validation: { required: false }, + }, + ], + }} + download={{ label: 'Download PDF' }} + onSubmit={handleSubmit} + onDownload={handleDownload} + onStateChange={handleStateChange} + onFieldChange={handleFieldChange} + documentHeight="500px" + /> + + {/* Event Log */} + {events.length > 0 && ( +
+
+ EVENT LOG +
+ {events.map((evt, i) => ( +
+ {evt} +
+ ))} +
+ )} +
+ + {/* Right Sidebar - Document Fields */}
-
- EVENT LOG + Document Fields +

+
+ {documentFieldsConfig.map((field) => ( +
+ + updateDocumentField(field.id, e.target.value)} + readOnly={field.readOnly} + style={{ + width: '100%', + padding: '8px 10px', + fontSize: '14px', + border: '1px solid #d1d5db', + borderRadius: '6px', + background: field.readOnly ? '#f3f4f6' : 'white', + color: field.readOnly ? '#6b7280' : '#111827', + cursor: field.readOnly ? 'not-allowed' : 'text', + boxSizing: 'border-box', + }} + /> +
+ ))}
- {events.map((evt, i) => ( -
- {evt} -
- ))}
- )} +
)} diff --git a/demo/src/CustomSignature.tsx b/demo/src/CustomSignature.tsx new file mode 100644 index 0000000..584a155 --- /dev/null +++ b/demo/src/CustomSignature.tsx @@ -0,0 +1,146 @@ +import React, { useEffect, useRef, useState } from 'react'; +import SignaturePad from 'signature_pad'; +import type { FieldComponentProps } from '@superdoc-dev/esign'; + +const CustomSignature: React.FC = ({ value, onChange, isDisabled, label }) => { + const [mode, setMode] = useState<'type' | 'draw'>('type'); + const canvasRef = useRef(null); + const signaturePadRef = useRef(null); + + const switchMode = (newMode: 'type' | 'draw') => { + setMode(newMode); + onChange(''); + if (newMode === 'draw' && signaturePadRef.current) { + signaturePadRef.current.clear(); + } + }; + + const clearCanvas = () => { + if (signaturePadRef.current) { + signaturePadRef.current.clear(); + onChange(''); + } + }; + + useEffect(() => { + if (!canvasRef.current || mode !== 'draw') return; + + signaturePadRef.current = new SignaturePad(canvasRef.current, { + backgroundColor: 'rgb(255, 255, 255)', + penColor: 'rgb(0, 0, 0)', + }); + + if (isDisabled) { + signaturePadRef.current.off(); + } + + signaturePadRef.current.addEventListener('endStroke', () => { + if (signaturePadRef.current) { + onChange(signaturePadRef.current.toDataURL()); + } + }); + + return () => { + if (signaturePadRef.current) { + signaturePadRef.current.off(); + } + }; + }, [mode, isDisabled, onChange]); + + return ( +
+ {label && ( + + )} +
+ + +
+ {mode === 'type' ? ( + onChange(e.target.value)} + disabled={isDisabled} + placeholder="Type your full name" + style={{ + fontFamily: 'cursive', + fontSize: '20px', + padding: '14px', + border: '1px solid #d1d5db', + borderRadius: '8px', + outline: 'none', + transition: 'border-color 0.2s', + }} + onFocus={(e) => (e.target.style.borderColor = '#14b8a6')} + onBlur={(e) => (e.target.style.borderColor = '#d1d5db')} + /> + ) : ( +
+ + +
+ )} +
+ ); +}; + +export default CustomSignature; diff --git a/demo/src/vite-env.d.ts b/demo/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/demo/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/demo/vite.config.ts b/demo/vite.config.ts index 597a3ac..884133b 100644 --- a/demo/vite.config.ts +++ b/demo/vite.config.ts @@ -1,10 +1,25 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import path from 'path'; -export default defineConfig({ +export default defineConfig(({ mode }) => ({ plugins: [react()], base: '/', resolve: { dedupe: ['react', 'react-dom'], + ...(mode === 'development' && { + alias: { + '@superdoc-dev/esign': path.resolve(__dirname, '../src/index.tsx'), + }, + }), }, -}); + server: { + proxy: { + '/v1': { + target: 'https://esign-demo-proxy-server-191591660773.us-central1.run.app', + changeOrigin: true, + secure: false, + }, + }, + }, +})); diff --git a/eslint.config.js b/eslint.config.js index cfbe3e5..965db51 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -24,5 +24,24 @@ export default [ 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn' } + }, + { + files: ['demo/server/**/*.js'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + fetch: 'readonly', + process: 'readonly', + Buffer: 'readonly', + console: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + setInterval: 'readonly', + clearInterval: 'readonly', + setImmediate: 'readonly', + global: 'readonly' + } + } } ]; diff --git a/src/index.tsx b/src/index.tsx index b15e8f9..c9971c0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -404,22 +404,34 @@ const SuperDocESign = forwardRef ({ - getState: () => ({ + useImperativeHandle( + ref, + () => ({ + getState: () => ({ + scrolled, + fields: fieldValues, + isValid, + isSubmitting, + }), + getAuditTrail: () => auditTrailRef.current, + reset: () => { + setScrolled(!document.validation?.scroll?.required); + setFieldValues(new Map()); + setIsValid(false); + auditTrailRef.current = []; + setAuditTrail([]); + }, + updateFieldInDocument, + }), + [ scrolled, - fields: fieldValues, + fieldValues, isValid, isSubmitting, - }), - getAuditTrail: () => auditTrailRef.current, - reset: () => { - setScrolled(!document.validation?.scroll?.required); - setFieldValues(new Map()); - setIsValid(false); - auditTrailRef.current = []; - setAuditTrail([]); - }, - })); + document.validation?.scroll?.required, + updateFieldInDocument, + ], + ); return (
diff --git a/src/types.ts b/src/types.ts index a20a00b..0b8b0bf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -100,6 +100,7 @@ export interface SuperDocESignHandle { getState: () => SigningState; getAuditTrail: () => AuditEvent[]; reset: () => void; + updateFieldInDocument: (field: FieldUpdate) => void; } export interface DownloadData {