Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Tests

on:
pull_request:
push:
branches: [master]
workflow_dispatch:

permissions:
contents: read

jobs:
php:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Requires PHP: 7.4 in the plugin header, plus current releases.
php: ["7.4", "8.2", "8.4"]
container: php:${{ matrix.php }}-cli
steps:
- uses: actions/checkout@v4

- name: Lint PHP
run: |
set -e
for f in $(find . -name '*.php' -not -path './.git/*'); do
php -l "$f"
done

- name: Webhook signature tests
run: php tests/test-webhook-signature.php

- name: Verification callback tests
run: php tests/test-verify-endpoint.php

- name: Shortcode asset tests
run: php tests/test-shortcode-assets.php
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ Open <http://localhost:8080> and complete the WordPress setup wizard.
2. Paste your UniLink URL (from [Didit Console](https://business.didit.me) → Workflow → Copy Link)
3. Save

> UniLink sessions are started from a shared URL, so the site cannot confirm who completed them. In UniLink mode results are shown in the button only: `[didit_gate]` stays locked, `[didit_status]` does not change and WooCommerce orders are not updated from the browser. Use API mode for any of those.

#### API mode (recommended for production)

1. Set **Mode** to `API Session`
Expand Down Expand Up @@ -290,7 +292,7 @@ wordpress-plugin/
├── languages/ # POT template + German translations (de_DE, de_DE_formal)
├── uninstall.php # Cleans up options on plugin deletion
├── readme.txt # WordPress.org plugin directory format
├── tests/ # Webhook signature tests (plain PHP, no dependencies)
├── tests/ # Webhook, completion callback and shortcode tests (plain PHP, no dependencies; run in CI)
├── docker-compose.yml # Local dev (WordPress + MySQL)
└── README.md
```
Expand All @@ -301,7 +303,8 @@ wordpress-plugin/

```
User clicks button → JS calls DiditSdk.startVerification({ url }) → Modal opens
→ User completes verification → onComplete fires → Button shows "Verified"
→ User completes verification → onComplete fires → Button shows the SDK result
(nothing is saved on the site)
```

### API Flow
Expand All @@ -313,8 +316,13 @@ User clicks button
→ PHP checks: nonce ✓ → login ✓ → rate limit ✓
→ PHP calls Didit API with API key (server-side) → returns { url }
→ JS calls DiditSdk.startVerification({ url }) → modal opens
→ User completes → onComplete fires → button shows "Verified"
→ JS sends POST /wp-json/didit/v1/verify → saves result to user meta
→ PHP records the session id on the user as pending (trusted session → user binding);
the session behind the user's current status stays bound until a new decision lands
→ User completes → onComplete fires
→ JS sends POST /wp-json/didit/v1/verify { sessionId }
→ PHP reads GET /v3/session/{id}/decision/ with the API key, checks the session
belongs to this user (or to the order being verified), saves Didit's status
→ Button shows the status the site saved
```

### WooCommerce Flow
Expand Down Expand Up @@ -390,7 +398,8 @@ When a user completes verification, the plugin saves these fields to WordPress u
| Meta key | Value | Description |
|----------|-------|-------------|
| `_didit_verified` | `1` | User is verified |
| `_didit_session_id` | UUID | Didit session ID |
| `_didit_session_id` | UUID | Didit session the stored status came from |
| `_didit_pending_session_id` | UUID (multi-value) | Sessions created since, still waiting for a decision |
| `_didit_status` | `Approved` / `Pending` / `Declined` | Verification result |
| `_didit_verified_at` | datetime | When verification was completed |

Expand Down Expand Up @@ -436,7 +445,7 @@ document.addEventListener('didit:complete', function (e) {
| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| `POST` | `/wp-json/didit/v1/session` | Create a verification session | CSRF nonce + login, or order key (after-purchase) |
| `POST` | `/wp-json/didit/v1/verify` | Save verification result to user/order meta | Login, or order key (after-purchase) |
| `POST` | `/wp-json/didit/v1/verify` | Confirm a completion with Didit and save its decision to user/order meta (API mode only) | Login, or order key (after-purchase) |
| `POST` | `/wp-json/didit/v1/webhook` | Didit webhook receiver (`status.updated`) | HMAC-SHA256 signature (`X-Signature-V2` → `X-Signature` → `X-Signature-Simple`) + timestamp |

## Uninstall
Expand All @@ -451,10 +460,12 @@ docker compose exec wordpress wp plugin install woocommerce --activate --allow-r

## Tests

Webhook signature verification is covered by a dependency-free PHP test suite:
Webhook signature verification, the browser completion callback and shortcode asset loading are covered by dependency-free PHP test suites:

```bash
php tests/test-webhook-signature.php
php tests/test-verify-endpoint.php
php tests/test-shortcode-assets.php
# or, without a local PHP:
docker run --rm -v "$PWD":/app -w /app php:8.2-cli php tests/test-webhook-signature.php
```
Expand Down
50 changes: 40 additions & 10 deletions assets/js/didit-verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,18 @@
var DiditSdk = window.DiditSDK.DiditSdk;
var i18n = cfg.i18n || {};

DiditSdk.shared.onComplete = function (result) {
var status = result.session ? result.session.status : "";

function setButtonState(type, status) {
document.querySelectorAll(".didit-verify-btn").forEach(function (btn) {
btn.classList.remove("didit-verified", "didit-declined", "didit-in-review");

if (result.type === "completed" && status === "Approved") {
if (type === "completed" && status === "Approved") {
btn.textContent = btn.dataset.success || "Verified";
btn.classList.add("didit-verified");
btn.disabled = true;
} else if (result.type === "completed" && status === "Declined") {
} else if (type === "completed" && status === "Declined") {
btn.textContent = btn.dataset.text || "Verify Identity";
btn.disabled = false;
} else if (result.type === "completed") {
} else if (type === "completed") {
btn.textContent = i18n.inReview || "Verification In Review";
btn.classList.add("didit-in-review");
btn.disabled = true;
Expand All @@ -29,6 +27,10 @@
btn.disabled = false;
}
});
}

DiditSdk.shared.onComplete = function (result) {
var status = result.session ? result.session.status : "";

var hidden = document.getElementById("didit_session_id");
if (hidden && result.session && result.session.sessionId) {
Expand All @@ -43,11 +45,19 @@
} catch (e) {}
}

if (cfg.restUrl && cfg.nonce) {
// In API mode the site confirms a completion with Didit before saving it, so
// the button shows the status the site stored rather than the one the SDK
// reported. UniLink results cannot be saved, so they are not sent.
var confirmWithSite = cfg.mode === "api" && cfg.restUrl && cfg.nonce;

if (!confirmWithSite || result.type !== "completed") {
setButtonState(result.type, status);
}

if (confirmWithSite) {
var verifyBody = {
type: result.type,
sessionId: result.session ? result.session.sessionId : "",
status: result.session ? result.session.status : ""
sessionId: result.session ? result.session.sessionId : ""
};
var orderBtn = document.querySelector(".didit-verify-btn[data-order-id]");
if (orderBtn) {
Expand All @@ -62,7 +72,27 @@
"X-WP-Nonce": cfg.nonce
},
body: JSON.stringify(verifyBody)
}).catch(function () {});
})
.then(function (r) {
return r.json().then(function (d) {
return { response: r, data: d };
});
})
.then(function (res) {
if (result.type !== "completed") return;
if (res.response.ok) {
setButtonState("completed", res.data.status);
} else if (res.response.status === 401) {
// Anonymous visitor outside an order: nothing is stored on the site.
setButtonState(result.type, status);
} else {
setButtonState("error", "");
alert((i18n.verificationError || "Verification error:") + " " + (res.data.message || "Error " + res.response.status));
}
})
.catch(function () {
if (result.type === "completed") setButtonState("error", "");
});
}

document.dispatchEvent(new CustomEvent("didit:complete", { detail: result }));
Expand Down
Loading
Loading