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 .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = {
'import/core-modules': [
'@woocommerce/blocks-registry',
'@woocommerce/settings',
'@wordpress/api-fetch',
'@wordpress/i18n',
'@wordpress/is-shallow-equal',
'@wordpress/element',
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
# Changelog
## 3.3.2 | 08-10-2026
Redesigned onboarding and settings experience.

### Version Changes
- [ADDED] A standalone Flutterwave admin screen under WooCommerce, replacing the WooCommerce gateway settings section as the place merchants configure the plugin.
- [ADDED] A three-step setup wizard for new installs — general details, API keys and webhook, then payment methods — followed by a confirmation screen. Each step saves on its own, so a merchant who drops out halfway keeps what they entered.
- [ADDED] A tabbed settings screen (General / API & Webhook / Payment Methods) for stores that are already configured. Merchants upgrading with keys already saved skip the wizard.
- [ADDED] Payment methods are now individual checkboxes (Cards, Stablecoin, Bank Transfer, Mobile Money, Apple Pay, Google Pay, Opay) instead of a single-choice dropdown.
- [ADDED] REST endpoints under `flutterwave/v1` backing the admin app, restricted to `manage_woocommerce`.
- [CHANGED] The plugin's Settings link and the old `wc-settings&tab=checkout&section=rave` URL now lead to the new screen.
- [CHANGED] Settings still persist to `woocommerce_rave_settings`, so the gateway, blocks integration and webhook handling are unchanged.
- [ADDED] The Flutterwave brand fonts — Millik for headings, Moderat for body text, with Inter beneath it — self-hosted from the plugin rather than loaded from Google Fonts and jsDelivr, so wp-admin makes no third-party font requests.
- [CHANGED] Logging and Barter toggles, and the payment options without a checkbox (USSD, QR, NQR, Credit, Barter) moved to an Advanced section on the Payment Methods tab. Existing values are preserved.
- [CHANGED] The secret hash now sits beside the webhook URL on the API & Webhook tab, with a Generate button.
- [SECURITY] Each store now gets its own generated secret hash. Stores still on the previous default must generate a new one (an admin notice links to it) before webhooks are accepted again.
## 3.3.1 | 08-09-2026
Security release.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ For FTP manual installation, [check here](https://wordpress.org/documentation/ar

- When in doubt about a transaction, always check the Flutterwave Dashboard to confirm the status of a transaction.
- Always ensure you keep your API keys securely and privately. Do not share with anyone.
- Ensure you change from the default secret hash on the Wordpress admin and apply same on the Flutterwave Dashboard.
- Ensure the secret hash on the API & Webhook tab matches the one on your Flutterwave Dashboard. The plugin generates a random one; use the Generate button to replace it at any time.
- Always ensure you install the most recent version of the Flutterwave WooCommerce plugin.


Expand Down
33 changes: 33 additions & 0 deletions assets/img/admin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Admin onboarding artwork

The onboarding wizard currently draws its illustrations and payment-method
marks as inline SVG in `client/admin/icons.js`. Those are placeholders sized and
coloured to the Figma frames so layout and spacing are final — only the artwork
itself is stand-in.

To swap in the real exports:

1. Drop the files here, e.g. `welcome-docs.svg`, `welcome-support.svg`,
`setup-success.svg`, `flutterwave-wordmark.svg`, `method-opay.svg`.
2. In `client/admin/icons.js`, replace the placeholder component body with an
image pointing at the file:

```js
import { adminData } from './lib/api';

export const DocsIllustration = () => (
<img
className="flw-illustration"
src={ `${ adminData().assetsUrl }welcome-docs.svg` }
alt=""
/>
);
```

`assetsUrl` already resolves to this directory and is passed through from
`Flutterwave_Admin_Page::enqueue_assets()`.
3. Run `npm run build:webpack`.

Keep the marks in `METHOD_MARKS` keyed by the method keys declared in
`client/admin/lib/constants.js`, since those keys are what the PHP settings
model maps to Flutterwave payment options.
210 changes: 210 additions & 0 deletions assets/img/admin/welcome-docs.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
373 changes: 373 additions & 0 deletions assets/img/admin/welcome-support.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
== Changelog ==
= 3.3.2 =
* Added: New Onboarding flow
= 3.3.1 =
* Security: Authenticate the payment callback against the order key; it previously accepted any order ID from an unauthenticated request.
* Security: Confirm cancellations with Flutterwave before changing an order status.
Expand Down
110 changes: 110 additions & 0 deletions client/admin/_fonts.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Self-hosted webfonts.
*
* Served from the plugin rather than Google Fonts and jsDelivr: no third-party
* requests from wp-admin (which keeps EU privacy reviews simple), and the files
* come from the same origin as the rest of the page. webpack fingerprints each
* file into build/fonts/, so the URLs below are rewritten at build time.
*
* Millik ships a single 400 weight — headings must not request bold or the
* browser synthesises it.
*/

@font-face {
font-family: "Millik";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./fonts/Millik.woff2") format("woff2");
}

@font-face {
font-family: "Moderat";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./fonts/Moderat-Regular.woff2") format("woff2");
}

@font-face {
font-family: "Moderat";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("./fonts/Moderat-Medium.woff2") format("woff2");
}

@font-face {
font-family: "Moderat";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("./fonts/Moderat-Bold.woff2") format("woff2");
}

/* Inter, the fallback beneath Moderat. Each subset keeps the unicode-range
Google generated, so a browser only downloads the ranges a page actually
uses — Moderat is Latin-only, so these carry non-Latin locales. */

@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("./fonts/Inter-cyrillic-ext-normal.woff2") format("woff2");
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("./fonts/Inter-cyrillic-normal.woff2") format("woff2");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("./fonts/Inter-greek-ext-normal.woff2") format("woff2");
unicode-range: U+1F00-1FFF;
}

@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("./fonts/Inter-greek-normal.woff2") format("woff2");
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}

@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("./fonts/Inter-vietnamese-normal.woff2") format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("./fonts/Inter-latin-ext-normal.woff2") format("woff2");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("./fonts/Inter-latin-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
118 changes: 118 additions & 0 deletions client/admin/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* Root of the Flutterwave admin app.
*
* Owns the settings state and decides which surface to show: the welcome
* screen for a fresh install, the wizard once activation starts, and the
* tabbed settings screen for anyone already configured.
*/

import { useState, useEffect, useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { LoadingScreen, Toast } from './components';
import Welcome from './screens/Welcome';
import Wizard from './screens/Wizard';
import Success from './screens/Success';
import Settings from './screens/Settings';
import { fetchSettings } from './lib/api';

const VIEW = {
LOADING: 'loading',
WELCOME: 'welcome',
WIZARD: 'wizard',
SUCCESS: 'success',
SETTINGS: 'settings',
};

const App = () => {
const [ view, setView ] = useState( VIEW.LOADING );
const [ values, setValues ] = useState( null );
const [ notice, setNotice ] = useState( null );

useEffect( () => {
let cancelled = false;

fetchSettings()
.then( ( payload ) => {
if ( cancelled ) {
return;
}
setValues( payload );
setView( payload.onboardingComplete ? VIEW.SETTINGS : VIEW.WELCOME );
} )
.catch( ( error ) => {
if ( cancelled ) {
return;
}
setNotice( {
status: 'error',
message:
error?.message ||
__( 'Could not load your Flutterwave settings.', 'rave-woocommerce-payment-gateway' ),
} );
setView( VIEW.WELCOME );
setValues( ( current ) => current || {} );
} );

return () => {
cancelled = true;
};
}, [] );

const setValue = useCallback( ( key, value ) => {
setValues( ( current ) => ( { ...current, [ key ]: value } ) );
}, [] );

// The API echoes the persisted state back, so trust it over local edits.
const onSaved = useCallback( ( payload ) => {
setValues( payload );

// The server-rendered warning about the old default hash would
// otherwise linger until the next page load.
if ( ! payload.secretHashIsLegacy ) {
document.getElementById( 'flw-legacy-secret-hash-notice' )?.remove();
}
}, [] );

const onNotice = useCallback( ( status, message ) => setNotice( { status, message } ), [] );

useEffect( () => {
if ( ! notice ) {
return undefined;
}

const timer = setTimeout( () => setNotice( null ), 4000 );
return () => clearTimeout( timer );
}, [ notice ] );

if ( VIEW.LOADING === view || ! values ) {
return <LoadingScreen />;
}

return (
<>
{ notice && (
<Toast status={ notice.status } message={ notice.message } onDismiss={ () => setNotice( null ) } />
) }

{ VIEW.WELCOME === view && <Welcome onActivate={ () => setView( VIEW.WIZARD ) } /> }

{ VIEW.WIZARD === view && (
<Wizard
values={ values }
setValue={ setValue }
onSaved={ onSaved }
onFinished={ () => setView( VIEW.SUCCESS ) }
onError={ ( message ) => onNotice( 'error', message ) }
/>
) }

{ VIEW.SUCCESS === view && <Success onDone={ () => setView( VIEW.SETTINGS ) } /> }

{ VIEW.SETTINGS === view && (
<Settings values={ values } setValue={ setValue } onSaved={ onSaved } onNotice={ onNotice } />
) }
</>
);
};

export default App;
Loading
Loading