diff --git a/.eslintrc.js b/.eslintrc.js index 7c8ad8e..135bd56 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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', diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b2ed61..bc546b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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§ion=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. diff --git a/README.md b/README.md index 5517af0..95df725 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/assets/img/admin/README.md b/assets/img/admin/README.md new file mode 100644 index 0000000..6a2e626 --- /dev/null +++ b/assets/img/admin/README.md @@ -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 = () => ( + + ); + ``` + + `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. diff --git a/assets/img/admin/welcome-docs.svg b/assets/img/admin/welcome-docs.svg new file mode 100644 index 0000000..a26766b --- /dev/null +++ b/assets/img/admin/welcome-docs.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/img/admin/welcome-support.svg b/assets/img/admin/welcome-support.svg new file mode 100644 index 0000000..d5874de --- /dev/null +++ b/assets/img/admin/welcome-support.svg @@ -0,0 +1,373 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/changelog.txt b/changelog.txt index 34ca29b..384751a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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. diff --git a/client/admin/_fonts.scss b/client/admin/_fonts.scss new file mode 100644 index 0000000..0089fd3 --- /dev/null +++ b/client/admin/_fonts.scss @@ -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; +} diff --git a/client/admin/app.js b/client/admin/app.js new file mode 100644 index 0000000..dd1279a --- /dev/null +++ b/client/admin/app.js @@ -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 ; + } + + return ( + <> + { notice && ( + setNotice( null ) } /> + ) } + + { VIEW.WELCOME === view && setView( VIEW.WIZARD ) } /> } + + { VIEW.WIZARD === view && ( + setView( VIEW.SUCCESS ) } + onError={ ( message ) => onNotice( 'error', message ) } + /> + ) } + + { VIEW.SUCCESS === view && setView( VIEW.SETTINGS ) } /> } + + { VIEW.SETTINGS === view && ( + + ) } + + ); +}; + +export default App; diff --git a/client/admin/components/index.js b/client/admin/components/index.js new file mode 100644 index 0000000..7fa5884 --- /dev/null +++ b/client/admin/components/index.js @@ -0,0 +1,297 @@ +/** + * Shared presentational components for the Flutterwave admin app. + */ + +import { useState, useRef, useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { ChevronLeft, ChevronDown, InfoIcon, CheckIcon } from '../icons'; + +/** + * Page chrome: the sticky breadcrumb bar plus the two-column body. + * + * @param {Object} props + * @param {string} props.backLabel Label shown next to the chevron. + * @param {Function} [props.onBack] Click handler; omit for a static crumb. + * @param {*} props.children Page body. + * @param {*} [props.aside] Right-hand rail (the stepper). + * @param {boolean} [props.wide] Let the main column fill the screen, for + * surfaces that centre or span their own + * content. + * @return {Element} The page shell. + */ +export const Page = ( { backLabel, onBack, children, aside, wide } ) => ( +
+
+
+ { onBack ? ( + + ) : ( + + + { backLabel } + + ) } +
+
+
+
{ children }
+ { aside &&
{ aside }
} +
+
+); + +/** + * "Step 1 of 3" pill. + * + * @param {Object} props + * @param {number} props.current One-based step index. + * @param {number} props.total Total steps. + * @return {Element} The pill. + */ +export const StepBadge = ( { current, total } ) => ( + + { /* translators: 1: current step number, 2: total number of steps. */ + __( 'Step', 'rave-woocommerce-payment-gateway' ) }{ ' ' } + + { current } { __( 'of', 'rave-woocommerce-payment-gateway' ) } { total } + + +); + +/** + * The progress rail on the right of each wizard step. + * + * @param {Object} props + * @param {Array} props.steps Step descriptors. + * @param {number} props.activeIndex Zero-based index of the current step. + * @return {Element} The stepper card. + */ +export const Stepper = ( { steps, activeIndex } ) => ( +
+ { steps.map( ( step, index ) => { + const done = index < activeIndex; + const active = index === activeIndex; + + return ( +
+ { ( done || active ) && } + { step.label } +
+ ); + } ) } +
+); + +/** + * Small "i" affordance that reveals help text on hover and focus. + * + * @param {Object} props + * @param {string} props.text Tooltip copy. + * @return {Element} The tooltip trigger. + */ +export const Tooltip = ( { text } ) => ( + + + { text } + +); + +/** + * Label + control wrapper with optional tooltip and error text. + * + * @param {Object} props + * @param {string} props.id Control id. + * @param {string} props.label Field label. + * @param {string} [props.tooltip] Help text. + * @param {string} [props.error] Validation message. + * @param {string} [props.hint] Persistent helper text below the control. + * @param {*} props.children The control itself. + * @return {Element} The field. + */ +export const Field = ( { id, label, tooltip, error, hint, children } ) => ( +
+ + { children } + { hint && ( +

+ + { hint } +

+ ) } + { error &&

{ error }

} +
+); + +/** + * Text input. + * + * @param {Object} props Standard input props. + * @return {Element} The input. + */ +export const TextInput = ( props ) => ; + +/** + * Multi-line input. + * + * @param {Object} props Standard textarea props. + * @return {Element} The textarea. + */ +export const TextArea = ( props ) =>