diff --git a/readme.md b/readme.md index 241acb9..f634578 100644 --- a/readme.md +++ b/readme.md @@ -2,245 +2,433 @@ ## Overview -This is a monorepo, containing 2 packages: +This is a monorepo containing packages that together provide a full membership and donation management system, delivered as a WordPress plugin. -- `packages/join-flow`: A React project (using `create-react-app`) implementing the join flow frontend. +- `packages/join-flow` — A React application implementing the join form frontend. +- `packages/join-block` — A WordPress Gutenberg plugin containing the join form block(s) and the backend logic that processes memberships, payments, and CRM integrations. +- `packages/join-e2e` — End-to-end tests (Puppeteer). -- `packages/join-block`: A WordPress Gutenberg block that allows the join flow to be dropped into a WordPress page, along with the backend join logic that communicates services to make the person a member. +**Current version:** 1.4.0 + +--- ## How does the Join Flow work? -We want to make the ability to join organisations widely available. +1. A visitor lands on a WordPress page with the **Join Form** block. They enter their email address and are directed to the join form itself. +2. The **Join Form Fullscreen Takeover** block hosts the React application. It takes over the full page and guides the visitor through a multi-step form: personal details, membership plan selection, optional donation, and payment. +3. On submission the React app POSTs to a WordPress REST API endpoint provided by the plugin. The backend orchestrates all downstream services: it creates the payment, sets up the subscription or one-off charge, and syncs the new member to any configured integrations (Auth0, Zetkin, Action Network, Mailchimp). +4. On success the visitor is redirected to a configurable success page. -To do so, it is useful for the ability to join to be neatly dropped into any page or post. Therefore this join flow is written as a WordPress block, that launches a form flow written in React. +--- -The general user flow, including technical detail is: +## WordPress Blocks -1. User visits a WordPress page with the "Join Form" block on it. This prompts the user for their email address and encourages them to join the organisation. -2. When they enter their email address and press the button they are directed to another page with the block "Join Form Fullscreen Takeover" on it. This is a React application that takes them through the join process and validates their details client side. -3. When the user is done with the form, the React application sends a POST request is sent to an special endpoint in the [WordPress REST API](https://developer.wordpress.org/rest-api/). This is setup by a WordPress plugin, which also adds the above mentioned blocks to the WordPress site. This handles the server side logic needed to make someone a member of the organisation. It creates them on Chargebee, sets up payment and then creates their user on Auth0. -4. On success, the React application is sent a JSON response. The user is redirected to a success page. This page can be any page on the WordPress site. This is setup when the "Join Form Fullscreen Takeover" block is setup. -5. All done! +| Block | Purpose | +|---|---| +| **Join Form** | Lightweight entry point — captures an email address and links to the full form page. | +| **Join Form Fullscreen Takeover** | Hosts the full multi-step React join flow. Add this to one page to enable membership sign-up. | -## WordPress Blocks included +All copy within both blocks is fully configurable — nothing is hardcoded. -They are designed to have the copy changed - nothing is hard coded. This is intended to allow the copy to be iterated to improve the performance of this landing page. +--- -- **Join Form Fullscreen Takeover** The whole join flow experience. Add this to one page and you are ready to allow someone to join. The React application takes over the whole page, so the rest of the page will be ignored. Also works on posts. +## Form Flows -## Build and Deployment Workflow +The join flow supports three distinct modes, controlled by environment variables: -### Build +### Standard join flow -Install dependencies and build +The default. Steps: -```bash -yarn -yarn composer -yarn build -``` +1. **Details** — name, email, phone, address (with optional postcode lookup), date of birth, nationality, county, "how did you hear about us?", contact consent, and any configured custom fields. +2. **Membership plan** — choose a tier from the configured plans. +3. **Donation** (optional) — offer an additional one-off or recurring donation on top of membership. +4. **Payment** — select a payment method and complete payment. +5. **Confirmation** — review and submit. -Results in deployable artifacts: +### Supporter / donation-focused mode (`REACT_APP_DONATION_SUPPORTER_MODE`) -- `packages/join-block`: Join block plugin +Reorders and simplifies the flow to: Donation → Details → Payment. The membership amount equals the donation amount; no separate plan selection step is shown. -## Auth0 Setup +### Update flow mode (`REACT_APP_IS_UPDATE_FLOW`) -You must create an Auth0 machine-to-machine application, and then authorize this application for the Auth0 Management API. -This is found in Applications => APIs => Auth0 Management API => Machine to Machine Applications. +For existing members updating their membership or payment details. The email is pre-filled via a URL parameter and the details step is skipped. -Once you have authorized the application, you must click the arrow to expand the authorization, and add the following -scopes: read:users, update:users, create:users, delete:users. +--- -### Deploying +## Payment Providers -#### Automated Deployment to WordPress.org +The plugin supports three payment providers. They can be enabled independently and combined (e.g. Stripe for cards and GoCardless for Direct Debit). -Releases to WordPress.org are automated via GitHub Actions. To deploy a new version: +### Stripe -1. **Bump the version** in all required files: +- Recurring subscriptions (card and Direct Debit) +- One-off donations via PaymentIntent (£0.01–£10,000) +- Customer upsert (creates or looks up existing customers) +- Subscription metadata (plan, organisation) +- Test and Live mode +- Environment variable: `REACT_APP_USE_STRIPE=true` -```bash -./scripts/bump-version.sh -``` +### GoCardless -This will automatically increment the patch version (e.g., 1.3.3 → 1.3.4). You can also specify a version manually: +- Recurring Direct Debit (UK/EU bank payments) +- Supports both Hosted Pages mode and API mode (requires GoCardless Pro account — `REACT_APP_USE_GOCARDLESS_API=true`) +- Automatic customer and mandate creation +- Sandbox and Live environments +- Environment variable: `REACT_APP_USE_GOCARDLESS=true` -```bash -./scripts/bump-version.sh 1.4.0 -``` +### Chargebee -2. **Review and commit the changes:** +- Subscription and billing management (multi-currency) +- Hosted Pages mode for PCI compliance +- API mode for direct customer/subscription creation +- Environment variable: `REACT_APP_USE_CHARGEBEE=true` -```bash -git diff -git add -A -git commit -m "Bump version to 1.3.4" -``` +--- -3. **Create and push the tag:** +## Integrations -```bash -git tag 1.3.4 -git push && git push --tags -``` +All integrations are optional. Enable and configure them via environment variables or the WordPress settings page. -4. **GitHub Actions takes over:** +| Integration | Purpose | +|---|---| +| **Auth0** | Creates user accounts with email, name, and role assignment on successful join. Requires an M2M application — see [Auth0 Setup](#auth0-setup). | +| **Zetkin** | Adds members to campaigns, applies plan-specific tags, syncs custom fields (DOB, "hear about us", contact preferences). | +| **Action Network** | Adds or updates people in Action Network; applies and removes tags; syncs custom fields. | +| **Mailchimp** | Adds subscribers to mailing lists with plan-specific tags. Manages "lapsed" tag on payment failure/recovery. | +| **Sentry** | Real-time error tracking for both frontend and backend. | +| **Google Cloud Logging** | Centralised log aggregation. | +| **Microsoft Teams** | Error alert notifications via incoming webhook. | +| **getAddress.io / ideal-postcodes** | UK postcode address lookup. | -The GitHub Action (`.github/workflows/release-plugin.yml`) will automatically: -- Build the plugin assets -- Package the plugin -- Deploy to the WordPress.org plugin repository +--- -You can monitor the deployment progress in the Actions tab on GitHub. +## Membership Lapsing -#### Manual Deployment +The plugin handles member lapsing automatically in response to payment provider webhooks. -In order to manually deploy this work, you need to create a WordPress plugin and add it to the WordPress instance as needed. +- A Stripe subscription entering `unpaid` or `incomplete_expired` triggers a lapse. +- A Stripe subscription returning to `active` triggers an unlapse. +- Lapsed/unlapsed status is reflected in all configured integrations (e.g. "lapsed" tag in Action Network, Mailchimp, Zetkin). -1. Run the build commands above to compile the React application. -2. Run `sh scripts/package.sh` on linux this can be run as `./scripts/package.sh`. This will create zip files of the WordPress plugin and theme in the root directory. -3. Upload them to a WordPress site and activate both. +--- -Note: the deployment script deletes this directory: `vendor/giggsey/libphonenumber-for-php/src/geocoding` because it is too large. -If someday we want to use the libphonenumber-for-php geocoding feature, we will need to restore the directory, and find -another way to reduce the size of the plugin zip (max 10MB). +## Developer Hooks -## Developer quickstart +The plugin exposes filters and actions for customisation without modifying plugin code. + +### Filters -### Running the whole system as a WordPress site +#### `ck_join_flow_should_lapse_member` -- Ensure you have a recent Node.js >= v18, Yarn, Composer and Docker installed. +Controls whether a member should be lapsed when a lapsing event is detected. -- Install dependencies +| Argument | Type | Description | +|---|---|---| +| `$default` | bool | `true` by default | +| `$email` | string | The member's email address | +| `$context` | array | `provider` (e.g. `stripe`), `trigger` (e.g. `invoice_payment_failed`), `event` (raw payload) | -```bash -yarn -yarn composer +```php +add_filter('ck_join_flow_should_lapse_member', function ($default, $email, $context) { + if (($context['provider'] ?? '') === 'gocardless') { + return false; + } + return $default; +}, 10, 3); ``` -- Copy the .env template into place, open it and add any missing configurations +#### `ck_join_flow_should_unlapse_member` + +Controls whether a member should be unlapsed when a reactivation event is detected. -```bash -cd packages/join-flow -cp .env.example .env +| Argument | Type | Description | +|---|---|---| +| `$default` | bool | `true` by default | +| `$email` | string | The member's email address | +| `$context` | array | Same shape as above | + +#### `ck_join_flow_add_tags` + +Customise tags applied across all integrations. + +| Argument | Type | Description | +|---|---|---| +| `$tags` | array | Tags to apply | +| `$data` | array | Form submission data | +| `$service` | string | Service name (e.g. `action_network`, `mailchimp`) | + +#### `ck_join_flow_action_network_add_tags` / `ck_join_flow_action_network_remove_tags` + +Action Network-specific tag overrides. + +### Actions + +#### `ck_join_flow_success` + +Fired after a successful join. + +| Argument | Type | Description | +|---|---|---| +| `$data` | array | Submitted form data | +| `$customer` | mixed | Customer object from the payment provider | + +#### `ck_join_flow_error` + +Fired when the join process encounters an error. + +| Argument | Type | Description | +|---|---|---| +| `$data` | array | Submitted form data | +| `$exception` | \Throwable | The exception that was thrown | + +#### `ck_join_flow_member_lapsed` + +Fired after a member has been successfully marked as lapsed. + +| Argument | Type | Description | +|---|---|---| +| `$email` | string | The member's email address | +| `$context` | array | Trigger context (see above) | + +```php +add_action('ck_join_flow_member_lapsed', function ($email, $context) { + // e.g. send an internal notification or update a CRM +}, 10, 2); ``` -- Boot the site +#### `ck_join_flow_member_unlapsed` + +Fired after a member has been successfully unmarked as lapsed. -```bash -yarn start +| Argument | Type | Description | +|---|---|---| +| `$email` | string | The member's email address | +| `$context` | array | Trigger context (see above) | + +--- + +## Auth0 Setup + +Create an Auth0 machine-to-machine application and authorise it for the Auth0 Management API. + +1. Go to **Applications > APIs > Auth0 Management API > Machine to Machine Applications**. +2. Authorise your application. +3. Expand the authorisation and add the following scopes: `read:users`, `update:users`, `create:users`, `delete:users`. + +--- + +## Configuration Reference + +Configuration is available through a WordPress settings page (**Settings > CK Join Flow**) or via environment variables. The `.env.example` files in each package list all available variables. + +### Key frontend environment variables + +| Variable | Description | +|---|---| +| `REACT_APP_ORGANISATION_NAME` | Displayed throughout the form | +| `REACT_APP_ORGANISATION_BANK_NAME` | Shown on Direct Debit pages | +| `REACT_APP_ORGANISATION_EMAIL_ADDRESS` | Contact email shown in the form | +| `REACT_APP_MEMBERSHIP_PLANS` | JSON array of membership plan objects | +| `REACT_APP_USE_STRIPE` | Enable Stripe | +| `REACT_APP_STRIPE_PUBLISHABLE_KEY` | Stripe public key | +| `REACT_APP_STRIPE_DIRECT_DEBIT_ONLY` | Restrict Stripe to Direct Debit only | +| `REACT_APP_USE_GOCARDLESS` | Enable GoCardless | +| `REACT_APP_USE_GOCARDLESS_API` | Use GoCardless API mode (requires Pro account) | +| `REACT_APP_USE_CHARGEBEE` | Enable Chargebee | +| `REACT_APP_CHARGEBEE_SITE_NAME` | Chargebee site name | +| `REACT_APP_CHARGEBEE_API_PUBLISHABLE_KEY` | Chargebee public key | +| `REACT_APP_USE_MAILCHIMP` | Enable Mailchimp integration | +| `REACT_APP_CREATE_AUTH0_ACCOUNT` | Enable Auth0 user creation | +| `REACT_APP_COLLECT_DATE_OF_BIRTH` | Collect date of birth | +| `REACT_APP_COLLECT_PHONE_AND_EMAIL_CONTACT_CONSENT` | Collect contact consent | +| `REACT_APP_ASK_FOR_ADDITIONAL_DONATION` | Show donation page | +| `REACT_APP_DONATION_SUPPORTER_MODE` | Enable supporter/donation-focused flow | +| `REACT_APP_IS_UPDATE_FLOW` | Enable existing-member update flow | +| `REACT_APP_USE_POSTCODE_LOOKUP` | Enable UK postcode address lookup | +| `REACT_APP_INCLUDE_SKIP_PAYMENT_BUTTON` | Allow skipping payment | +| `REACT_APP_HIDE_ZERO_PRICE_DISPLAY` | Hide £0 price labels | +| `REACT_APP_USE_TEST_DATA` | Pre-fill form with test data | +| `REACT_APP_SENTRY_DSN` | Sentry DSN for frontend error tracking | + +### Membership plans format + +```json +[ + { + "value": "standard", + "label": "Standard", + "amount": "10", + "frequency": "monthly", + "currency": "GBP", + "allowCustomAmount": false, + "add_tags": ["member"], + "remove_tags": [] + } +] ``` -#### To use join form 'in-place' in a WordPress site +### Key backend environment variables -- Open and enable the 'Join' plugin. +| Variable | Description | +|---|---| +| `STRIPE_SECRET_KEY` | Stripe secret key | +| `GC_ACCESS_TOKEN` | GoCardless API token | +| `GC_ENVIRONMENT` | `sandbox` or `live` | +| `CHARGEBEE_SITE_NAME` | Chargebee site name | +| `CHARGEBEE_API_KEY` | Chargebee API key | +| `AUTH0_DOMAIN` | Auth0 tenant domain | +| `AUTH0_CLIENT_ID` | Auth0 M2M app client ID | +| `AUTH0_CLIENT_SECRET` | Auth0 M2M app client secret | +| `AUTH0_MANAGEMENT_AUDIENCE` | Auth0 Management API identifier | +| `MICROSOFT_TEAMS_INCOMING_WEBHOOK` | Teams webhook URL for error alerts | +| `SENTRY_DSN` | Sentry DSN for backend error tracking | +| `GOOGLE_CLOUD_PROJECT_ID` | Google Cloud project ID | +| `GOOGLE_CLOUD_KEY_FILE_CONTENTS` | Google Cloud service account credentials (JSON) | +| `DEBUG_JOIN_FLOW` | Set to `true` to load frontend from `localhost:3000` | -- Add the "Join Form Fullscreen Takeover" block to a WordPress page. This will be where the join form itself will live. It can be linked to directly. Save this page. +--- -- Wherever you want the join form to be launched from, add the "Join Form" WordPress block. This allows the email address to be pre-filled for the person wanting to join. Connect this to the page you have just created. Save the post. +## Tech Stack -- You will now have a working join form that is working from the code on your machine. If you modify the code in `packages/join-flow` this will update the join flow. If you modify the code in `packages/join-block` this will change the backend logic of the WordPress plugin. +### Frontend -#### To work on the join form as a self-contained React application (with live-reload, etc) +- React 16, React Bootstrap +- React Hook Form, Yup (form state and validation) +- Stripe.js / `@stripe/react-stripe-js` +- Chargebee.js +- Webpack 5, Babel 7, SASS +- Jest, Testing Library -- Open +### Backend -### Running the front end in isolation (without a backend) +- PHP 8+, WordPress +- Carbon Fields (admin UI) +- Stripe PHP SDK, GoCardless PHP SDK, Chargebee PHP SDK +- Auth0 PHP SDK +- Mailchimp Marketing SDK +- GuzzleHTTP, Monolog, Google Cloud Logging +- PHPUnit, Brain Monkey, Mockery -- Ensure you have a recent Node.js >= v18 and Yarn installed. +### Infrastructure -- Install dependencies +- Lerna (monorepo) +- Docker Compose (local WordPress environment) +- GitHub Actions (CI/CD, WordPress.org releases) + +--- + +## Build and Deployment + +### Build ```bash yarn +yarn composer +yarn build ``` -- Copy the .env template into place, open it and add any missing configurations +This produces a deployable WordPress plugin in `packages/join-block`. + +### Deploying to WordPress.org (automated) + +Releases are automated via GitHub Actions (`.github/workflows/release-plugin.yml`). + +1. Bump the version: ```bash -cd packages/join-flow -cp .env.example .env +./scripts/bump-version.sh # auto-increments patch +./scripts/bump-version.sh 1.4.0 # or specify a version ``` -- Boot the site +2. Commit and tag: ```bash -yarn run frontend +git add -A +git commit -m "Bump version to 1.4.0" +git tag 1.4.0 +git push && git push --tags ``` -- Open +The GitHub Action will build, package, and deploy to the WordPress.org plugin repository automatically. Monitor progress in the Actions tab. -## Developer Hooks +### Manual deployment -The plugin exposes filters and actions so you can customise lapsing behaviour without modifying plugin code. +```bash +yarn && yarn composer && yarn build +sh scripts/package.sh +``` -### Filters +Upload the resulting zip to a WordPress site and activate the plugin. -#### `ck_join_flow_should_lapse_member` +> **Note:** The packaging script removes `vendor/giggsey/libphonenumber-for-php/src/geocoding` to stay under the 10 MB WordPress.org plugin size limit. -Controls whether a member should be lapsed when a lapsing event is detected (e.g. a Stripe subscription becomes `unpaid` or `incomplete_expired`). Return `false` to prevent the lapse. +--- -| Argument | Type | Description | -|---|---|---| -| `$default` | bool | `true` by default | -| `$email` | string | The member's email address | -| `$context` | array | Trigger context: `provider` (e.g. `stripe`), `trigger` (e.g. `invoice_payment_failed`), `event` (raw payload) | +## Developer Quickstart -```php -add_filter('ck_join_flow_should_lapse_member', function ($default, $email, $context) { - // Prevent lapsing if triggered by a GoCardless event. - if (($context['provider'] ?? '') === 'gocardless') { - return false; - } - return $default; -}, 10, 3); +### Full stack (WordPress + React) + +**Prerequisites:** Node.js >= 18, Yarn, Composer, Docker + +```bash +# Install dependencies +yarn +yarn composer + +# Configure the frontend +cd packages/join-flow +cp .env.example .env +# Edit .env — add API keys and enable desired features + +# Start the Docker stack +cd ../.. +yarn start ``` -#### `ck_join_flow_should_unlapse_member` +- WordPress admin: `http://localhost:8082/wp-admin` +- React dev server (live reload): `http://localhost:3000` -Controls whether a member should be unlapsed when a reactivation event is detected (e.g. a Stripe subscription returns to `active`). Return `false` to prevent the unlapse. +**WordPress setup:** -| Argument | Type | Description | -|---|---|---| -| `$default` | bool | `true` by default | -| `$email` | string | The member's email address | -| `$context` | array | Same shape as above | +1. Go to `http://localhost:8082/wp-admin/plugins.php` and activate the **Join** plugin. +2. Add the **Join Form Fullscreen Takeover** block to a page — this is where the join flow lives. +3. Optionally add the **Join Form** block elsewhere to pre-fill the email and link through. +4. Configure credentials and feature flags at **Settings > CK Join Flow**. -```php -add_filter('ck_join_flow_should_unlapse_member', function ($default, $email, $context) { - return $default; -}, 10, 3); +### Frontend only (no backend) + +```bash +yarn +cd packages/join-flow +cp .env.example .env +yarn run frontend +# Open http://localhost:3000 ``` -### Actions +Use `REACT_APP_USE_TEST_DATA=true` in `.env` to pre-fill the form with example data. -#### `ck_join_flow_member_lapsed` +--- -Fired after a member has been successfully marked as lapsed in all configured integrations. +## Testing -| Argument | Type | Description | -|---|---|---| -| `$email` | string | The member's email address | -| `$context` | array | Trigger context (see above) | +### Frontend unit tests -```php -add_action('ck_join_flow_member_lapsed', function ($email, $context) { - // Send an internal notification, update a CRM, etc. -}, 10, 2); +```bash +cd packages/join-flow +yarn test ``` -#### `ck_join_flow_member_unlapsed` +### Backend unit tests -Fired after a member has been successfully unmarked as lapsed in all configured integrations. +```bash +cd packages/join-block +composer test +``` -| Argument | Type | Description | -|---|---|---| -| `$email` | string | The member's email address | -| `$context` | array | Trigger context (see above) | +### End-to-end tests -```php -add_action('ck_join_flow_member_unlapsed', function ($email, $context) { - // Send a welcome-back notification, etc. -}, 10, 2); +```bash +cd packages/join-e2e +yarn test ```