Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ playwright-report/
src/content/docs/spec/
src/content/docs/reference/
src/generated/
public/woocommerce-checkout-postcode-field.png
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule "php"]
path = php
url = https://github.com/gatepost-dev/php
[submodule "woocommerce"]
path = woocommerce
url = https://github.com/gatepost-dev/woocommerce
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pnpm-lock.yaml
spec
js
php
woocommerce
dist
temp
coverage
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This repo holds the source of the site at <https://gatepost-dev.github.io/docs/>
## What the site holds

- A [playground](https://gatepost-dev.github.io/docs/playground/) where you check a postcode in your browser.
- Guides for the TypeScript core, the TypeScript client, the PHP package, the HTML field and the React field.
- Guides for the TypeScript core, the TypeScript client, the PHP package, the HTML field, the React field and the WooCommerce plugin.
- An API reference that the build writes from the TSDoc comments and the PHP doc comments.
- The spec pages: the grammar, the client contract, the field and the glossary.
- A [readiness tracker](https://gatepost-dev.github.io/docs/readiness/) of common tools that reject the new postcode.
Expand All @@ -44,7 +44,7 @@ pnpm sdk
pnpm dev
```

`pnpm sdk` builds the `js` and `php` submodules, because the site documents their source. `pnpm dev` serves the site at `http://localhost:4321/docs/`.
`pnpm sdk` builds the `js` and `php` submodules, because the site documents their source. The `woocommerce` submodule needs no build. The site copies one screenshot from it. `pnpm dev` serves the site at `http://localhost:4321/docs/`.

## Add a page

Expand Down
5 changes: 5 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import starlight from '@astrojs/starlight';
import { defineConfig } from 'astro/config';
import { createStarlightTypeDocPlugin } from 'starlight-typedoc';
import { writeClientSamples } from './scripts/client-samples.ts';
import { copyPluginImage } from './scripts/plugin-assets.ts';
import { writeSpecPages } from './scripts/spec-pages.ts';

// GitHub Pages serves the org's project site under the repo name. A custom domain changes the
Expand All @@ -26,6 +27,9 @@ function typeDocOf(name) {
sidebar: { label: `@gatepost/${name}`, collapsed: true },
typeDoc: {
excludeInternal: true,
// The field element inherits every member of HTMLElement. Without this, its page lists them
// all, grows past 1 MB, and the accessibility check of that page runs out of time.
excludeExternals: true,
readme: 'none',
disableSources: true,
entryFileName: 'index',
Expand All @@ -45,6 +49,7 @@ export default defineConfig({
// The preview serves the built site and reads none of the generated files.
if (command === 'preview') return;
writeSpecPages(import.meta.dirname);
copyPluginImage(import.meta.dirname);
await writeClientSamples(import.meta.dirname);
try {
execFileSync(
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default defineConfig(
'spec/**',
'js/**',
'php/**',
'woocommerce/**',
'test-results/**',
'playwright-report/**',
],
Expand Down
24 changes: 24 additions & 0 deletions scripts/plugin-assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2026 The Gatepost authors
// SPDX-License-Identifier: Apache-2.0
// Copies the checkout screenshot of the WooCommerce plugin from its submodule, so that the guide
// shows the image that the plugin shows, from this site and from no other.
import { copyFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';

/** The screenshot in the plugin submodule. It shows synthetic values. */
export const PLUGIN_IMAGE = 'assets/checkout-postcode-field.png';

const COPY = 'public/woocommerce-checkout-postcode-field.png';

/**
* Copies the screenshot into the public folder of the site, which the build serves as it is.
* The build calls it before it reads the pages.
*
* @param root - The root folder of the docs repo, which holds the `woocommerce` submodule.
* @param out - The folder that receives the copy. It is the root folder by default.
*/
export function copyPluginImage(root: string, out: string = root): void {
const target = join(out, COPY);
mkdirSync(join(target, '..'), { recursive: true });
copyFileSync(join(root, 'woocommerce', PLUGIN_IMAGE), target);
}
106 changes: 106 additions & 0 deletions src/content/docs/guides/woocommerce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
title: Add the postcode to a WooCommerce checkout
description: Install Gatepost Postcode for WooCommerce, and choose how it checks each postcode.
sidebar:
label: WooCommerce
order: 6
---

Gatepost Postcode for WooCommerce adds a postcode field to the checkout for Nigerian addresses. WooCommerce hides that field for Nigeria. The plugin shows it again, and it checks what the customer types.

The field shows only when the address is in Nigeria. It works in the classic checkout and in the checkout block. The plugin needs PHP 8.1, WordPress 6.7 and WooCommerce 10.0, or later versions of each.

## Install

The plugin is not published yet. It has no release, and it is not on WordPress.org. There is no zip file to download today.

To try the plugin now, build the zip file from the source. You need Git, PHP 8.1 or later and Composer.

```sh notrun
git clone https://github.com/gatepost-dev/woocommerce
cd woocommerce
composer install
scripts/build-zip
```

The script writes `dist/gatepost-postcode-for-woocommerce.zip`. Do not use the source archive that GitHub offers. It does not hold the bundled SDK, so the plugin cannot start.

Install and activate WooCommerce first. Then, in WordPress, go to Plugins > Add New Plugin > Upload Plugin, upload the zip file, and activate it.

## Choose the settings

The settings are under WooCommerce > Settings > Advanced > Nigerian postcodes. The name of the section is `gatepost_postcode`. A store owner never types a name from the table. The names help a developer find each setting in the code. All are option names, except `gatepost_wc_remove_key`. It is a field of the form, and WooCommerce does not save it.

<div class="table-scroll" role="region" tabindex="0" aria-label="Table: Settings">

| Setting | Name | Values | Default |
| --------------------- | ------------------------ | --------------------------------------------------------------------------------------- | -------- |
| Live secret key | `gatepost_wc_secret_key` | A key that starts with `nipost_live_` | empty |
| Remove the key | `gatepost_wc_remove_key` | A box that removes the saved key. It shows only when a key is saved. | off |
| Required | `gatepost_wc_required` | A box. A customer in Nigeria must enter a postcode. | off |
| Old 6-digit postcodes | `gatepost_wc_legacy` | `accept` ("Accept them") or `reject` ("Ask for the new postcode") | `accept` |
| Lookup | `gatepost_wc_confirm` | `level1` ("Ask NIPOST whether the postcode exists") or `none` ("Check the format only") | `level1` |

</div>

The plugin never prints the saved key. The key field stays empty, and a line tells you that a key is saved. An empty field keeps the saved key. To remove the key, tick the box "Remove the key" and save. A new key that you type in the same save replaces the old key.

The plugin saves only a live secret key. NIPOST's test keys work only on its staging gateway, which the plugin does not use. A key that does not start with `nipost_live_` gives an error, and the saved key stays as it was. Read [how to get a key](../../start/#get-a-key). The plugin does not issue keys.

## What the customer sees

A customer with a Nigerian address sees a Postcode field. The plugin checks the format while the customer fills in the form. It names the problem and suggests a fix for a common typo, such as the letter O in place of a zero.

![The billing details of the classic checkout, with Nigeria as the country and the postcode FC 01 Z99 ZZ 01 in the Postcode field. The name, address and email are made-up examples.](/docs/woocommerce-checkout-postcode-field.png)

The plugin stores the postcode in its standard form, such as `FC-01-Z99-ZZ-01`. It copies the postcode into the address of the order, so shipping plugins see it. The orders list shows the postcode and the result of the check. The plugin works with order tables (HPOS) and with orders stored as posts.

The format check needs no key. It runs on your server and sends nothing to NIPOST.

## Look up each postcode

With a live secret key and the lookup on, the plugin asks NIPOST whether each postcode exists. It asks after the customer places the order, not while the customer types. It sends one request for each different postcode of the order. The billing and shipping addresses share one request when they hold the same postcode.

A payment retry reuses the order. The order remembers the answer for each address. A retry sends only a postcode that has no answer yet, or a postcode that changed. The plugin never retries by itself.

NIPOST receives these items:

- The postcode in its standard form.
- Your secret key, in the `X-API-Key` header.
- The IP address of your shop server. NIPOST sees it with each request.

The plugin sends no name, email address or other part of the address. It never sends an old 6-digit postcode. Read NIPOST's [terms of use](https://postcode.gov.ng/terms), [acceptable use policy](https://postcode.gov.ng/acceptable-use) and [privacy policy](https://postcode.gov.ng/privacy) before you enter a key.

## Read the result of a check

The order keeps the result in the order meta `_gatepost_postcode_check`. An order with two addresses gets the status that needs the most attention.

<div class="table-scroll" role="region" tabindex="0" aria-label="Table: Check status">

| Status | Meaning |
| ----------- | ------------------------------------------------------------------------------------------- |
| `valid` | NIPOST knows the postcode. |
| `invalid` | NIPOST has no record of the postcode. An order note asks you to check it with the customer. |
| `unchecked` | The plugin did not ask NIPOST. The store has no key, or the lookup is off. |
| `error` | The lookup failed. An order note gives the cause. The orders list shows "Check failed". |

</div>

An old 6-digit postcode that the store accepts also gets `unchecked`, because NIPOST never receives it.

## When the lookup fails

A failed lookup never stops an order. The plugin waits at most 3 seconds for each postcode. The order keeps the postcode that the customer typed, with the status `unchecked` or `error`. An order note names the true cause, and it asks you to check the postcode by hand.

NIPOST's service can be slow or down, and a store must not lose a sale for that reason. A postcode that NIPOST does not know also does not stop the order. It gets the status `invalid` and a note.

The notes and the WooCommerce log name a postcode with its last part hidden, such as `FC-01-Z99-ZZ-**`.

## Export and erase requests

The plugin answers the export and erase tools of WordPress. The export holds the postcodes of an order and of a customer's saved address. An erase request removes them. It keeps the check status, which is not personal data. The plugin also adds suggested text to the privacy policy page of your store.

## Next steps

- [Read the "External services" section of the plugin's `readme.txt`](https://github.com/gatepost-dev/woocommerce/blob/main/readme.txt) for the full text of what it sends to NIPOST.
- [Read the postcode grammar](../../spec/grammar/) that the format check follows.
15 changes: 8 additions & 7 deletions src/content/docs/start.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ Each Gatepost tool follows the same spec, so each one reads a postcode in the sa

<div class="table-scroll" role="region" tabindex="0" aria-label="Table: Gatepost tools">

| You want to | Use | Needs a key |
| -------------------------------------------------------- | -------------------------------------------------- | ---------------- |
| Check the form of a postcode in TypeScript or JavaScript | [`@gatepost/core`](../guides/typescript/) | no |
| Add a postcode field to an HTML form | [`@gatepost/field`](../guides/html/) | only to confirm |
| Add a postcode field to a React form | [`@gatepost/react`](../guides/react/) | only to confirm |
| Look up a postcode from a TypeScript server | [`@gatepost/client`](../guides/typescript-client/) | yes |
| Check or look up a postcode in PHP | [`gatepost/postcode`](../guides/php/) | only for lookups |
| You want to | Use | Needs a key |
| -------------------------------------------------------- | ----------------------------------------------------------- | ---------------- |
| Check the form of a postcode in TypeScript or JavaScript | [`@gatepost/core`](../guides/typescript/) | no |
| Add a postcode field to an HTML form | [`@gatepost/field`](../guides/html/) | only to confirm |
| Add a postcode field to a React form | [`@gatepost/react`](../guides/react/) | only to confirm |
| Look up a postcode from a TypeScript server | [`@gatepost/client`](../guides/typescript-client/) | yes |
| Check or look up a postcode in PHP | [`gatepost/postcode`](../guides/php/) | only for lookups |
| Check postcodes in a WooCommerce checkout | [Gatepost Postcode for WooCommerce](../guides/woocommerce/) | only for lookups |

</div>

Expand Down
3 changes: 2 additions & 1 deletion src/release-state.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"state": "not published"
"state": "not published",
"plugin": "not published"
}
31 changes: 30 additions & 1 deletion test/unit/release-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ import { trackedPages } from '../../scripts/check-prose.ts';
// from each guide and sets the state in src/release-state.json to "published".
const NOTICE = 'The first alpha is not published yet.';

const { state } = JSON.parse(readFileSync('src/release-state.json', 'utf8')) as { state: string };
// The sentence that the WooCommerce guide holds while the plugin has no release. The plugin has its
// own flag, because it ships on its own schedule. The guide gives no install line of a registry.
const PLUGIN_NOTICE = 'The plugin is not published yet.';
const PLUGIN_PAGE = 'src/content/docs/guides/woocommerce.md';

const { state, plugin } = JSON.parse(readFileSync('src/release-state.json', 'utf8')) as {
state: string;
plugin: string;
};

const INSTALL_PAGES = [
...new Set(
Expand Down Expand Up @@ -46,3 +54,24 @@ describe('the release state', () => {
expect(others.filter((page) => readFileSync(page, 'utf8').includes(NOTICE))).toEqual([]);
});
});

describe('the release state of the plugin', () => {
it('is one of the two known values', () => {
expect(['published', 'not published']).toContain(plugin);
});

it('matches the notice of the WooCommerce guide', () => {
const text = readFileSync(PLUGIN_PAGE, 'utf8');
expect(
text.includes(PLUGIN_NOTICE),
plugin === 'not published'
? 'the guide must say that the plugin is not published'
: 'the guide still says that the plugin is not published',
).toBe(plugin === 'not published');
});

it('keeps the notice off every other page', () => {
const others = trackedPages().filter((page) => page !== PLUGIN_PAGE);
expect(others.filter((page) => readFileSync(page, 'utf8').includes(PLUGIN_NOTICE))).toEqual([]);
});
});
Loading