diff --git a/.distignore b/.distignore index 0351edb..0b3643e 100644 --- a/.distignore +++ b/.distignore @@ -37,4 +37,20 @@ playwright.config.js /node_modules /vendor rave-payment-forms.zip - +/client +eslint.config.cjs +phpcs.xml.dist +phpunit.xml.dist +phpunit-integration.xml.dist +.npmpackagejsonlintrc.json +/coverage +/playwright-report +/test-results +/.phpunit.cache +# Old integrations scaffolding, replaced by includes/integrations/class-flw-integrations.php. +src/Integrations +includes/integrations/class-flw-thirdparty-integrations.php +views/admin-integrations-page.php +assets/css/admin +assets/js/admin +/test-reports diff --git a/.env.e2e.example b/.env.e2e.example deleted file mode 100644 index b7ae661..0000000 --- a/.env.e2e.example +++ /dev/null @@ -1,17 +0,0 @@ -# Store Base Url -BASEURL="https://flutterwave-payments.ddev.site" -# product to be bought -SIMPLE_PAYMENT_FORM_URL="/payment-form" -DONATION_PAYMENT_FORM_URL="/donation-form" -TICKET_PAYMENT_FORM_URL="/ticket-form" - -# test customer credentials -CUSTOMER_EMAIL="developers@flutterwavego.com" -CUSTOMER_PASSWORD="password" - -# Credit Card Information -CREDIT_CARD_NUMBER="5531886652142950" -CREDIT_CARD_EXPIRATION="09/32" -CREDIT_CARD_CVV="564" -CREDIT_CARD_PIN="3310" -CREDIT_CARD_OTP="12345" \ No newline at end of file diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 49d1cec..0000000 --- a/.eslintignore +++ /dev/null @@ -1,12 +0,0 @@ -!.eslintrc.js -build -assets -bin -docs -includes -i18n/languages -release -tasks -templates -vendor -tests \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 41364c8..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - extends: [], - rules: { - // You can use the 'rules' key to override specific settings in the WooCommerce plugin - '@wordpress/i18n-translator-comments': 'warn', - '@wordpress/valid-sprintf': 'warn', - 'jsdoc/check-tag-names': [ - 'error', - { definedTags: ['jest-environment'] }, - ], - }, - settings: { - // List of modules that are externals in our webpack config. - // This helps the `import/no-extraneous-dependencies` and - //`import/no-unresolved` rules account for them. - 'import/core-modules': [ - '@wordpress/i18n', - '@wordpress/is-shallow-equal', - '@wordpress/element', - ], - }, -}; diff --git a/.github/actions/make-build/action.yml b/.github/actions/make-build/action.yml index aa586ee..eb03ba7 100644 --- a/.github/actions/make-build/action.yml +++ b/.github/actions/make-build/action.yml @@ -22,7 +22,7 @@ runs: env: RELEASE_FILENAME: 'rave-payment-forms.zip' run: | - npm install + npm ci npm run build if [[ ! -f $RELEASE_FILENAME ]]; then diff --git a/.github/workflows/change-review.yml b/.github/workflows/change-review.yml deleted file mode 100644 index 8066641..0000000 --- a/.github/workflows/change-review.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Review changes on Dev (Commits/PRs) - -on: - push: - branches: ['master'] - pull_request: - branches: ['dev'] - types: [opened, synchronize, reopened] - -env: - WP_MIN_SUPPORTED_VERSION: '6.0' - PHP_MIN_SUPPORTED_VERSION: '7.4' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - generate-wp-compat-matrix: - name: 'Generate the matrix for wordpress compatibility dynamically' - runs-on: ubuntu-20.04 - outputs: - matrix: ${{ steps.generate_matrix.outputs.matrix }} - steps: - - name: 'Generate matrix' - id: generate_matrix - run: | - MATRIX_INCLUDE=$( echo "[{\"wordpress\":\"$WP_MIN_SUPPORTED_VERSION\",\"gutenberg\":\"13.6.0\",\"php\":\"$PHP_MIN_SUPPORTED_VERSION\"}]" ) - echo "matrix={\"wordpress\":[\"latest\"],\"gutenberg\":[\"latest\"],\"php\":[\"7.4\"], \"include\":$MATRIX_INCLUDE}" >> $GITHUB_OUTPUT - phpcs: - name: PHPCS - strategy: - matrix: - operating-system: ['ubuntu-latest'] - php-versions: ['7.4', '8.0', '8.1'] - phpunit-versions: ['7.5.20'] - runs-on: ${{ matrix.operating-system }} - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: 'Validate composer.json and composer.lock' - run: composer validate --strict - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - ini-values: 'memory_limit=1G' - coverage: none - tools: cs2pr - - name: Install Composer dependencies - uses: 'ramsey/composer-install@v2' - - name: Run PHPCS checks - continue-on-error: true - run: vendor/bin/phpcs --ignore=*/tests/*,*/data/*,*/assets/* --report-full . - - name: Git Blame - continue-on-error: true - run: vendor/bin/phpcs -s --report=gitblame --ignore=*/tests/*,*/data/*,*/assets/* . - - name: Run WordPress Checks - continue-on-error: true - run: composer run standards:check diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c8bb59e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,157 @@ +name: CI + +on: + push: + branches: ['master', 'dev'] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.3' + coverage: none + tools: cs2pr + - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci --ignore-scripts + - name: PHP coding standards + run: vendor/bin/phpcs -q --report=checkstyle | cs2pr + - name: JavaScript + run: npm run lint:js + - name: CSS + run: npm run lint:css + - name: package.json + run: npm run lint:pkg-json + - name: JavaScript unit tests + run: npm run test:js + - name: Build admin app + run: npm run build:admin + + audit: + name: Dependency audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.3' + coverage: none + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + - name: npm audit + run: npm audit --audit-level=moderate + - name: composer audit + run: composer audit --locked + + unit: + name: Unit (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.1', '8.3', '8.4', '8.5'] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ matrix.php }} + coverage: ${{ matrix.php == '8.3' && 'xdebug' || 'none' }} + - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + - name: Run unit tests + if: matrix.php != '8.3' + run: vendor/bin/phpunit --testsuite unit + - name: Run unit tests with coverage + if: matrix.php == '8.3' + run: vendor/bin/phpunit --testsuite unit --coverage-text + + integration: + name: Integration (WP ${{ matrix.wp }}, PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - wp: latest + php: '8.3' + - wp: latest + php: '7.4' + - wp: '6.4' + php: '8.1' + env: + WP_ENV_PHP_VERSION: ${{ matrix.php }} + # '' is falsy in expressions, so the branch must be the && operand, not the || fallback. + WP_ENV_CORE: ${{ matrix.wp != 'latest' && format('WordPress/WordPress#{0}-branch', matrix.wp) || '' }} + # Only the tests site is used here; the EDD and GiveWP activation in .wp-env.json is for e2e. + WP_ENV_LIFECYCLE_SCRIPT_AFTER_START: wp-env run tests-cli wp plugin activate rave-payment-forms + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '7.4' + coverage: none + - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci --ignore-scripts + # build/ is not committed; without it the admin app tests are skipped. + - run: npm run build:admin + # wp-env activates every listed plugin on start, and EDD/GiveWP require newer WordPress than the matrix floor. + - run: | + echo '{ "plugins": [] }' > .wp-env.override.json + - run: npm run env:start + - run: npm run test:integration + + e2e: + name: End-to-end + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '7.4' + coverage: none + - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + composer-options: '--no-dev' + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci --ignore-scripts + # build/ is not committed; the admin screens and blocks need it. + - run: npm run build:admin + - run: npm run test:e2e:install + - run: npm run env:start + - run: npm run test:e2e + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + id: report + if: always() + with: + name: playwright-report + path: | + playwright-report + test-reports + test-results + retention-days: 7 + if-no-files-found: warn + - name: Summary + if: always() + run: node tests/e2e/report/summary.js test-reports/e2e-results.json "${{ steps.report.outputs.artifact-url }}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/e2e-report.yml b/.github/workflows/e2e-report.yml new file mode 100644 index 0000000..b5156f9 --- /dev/null +++ b/.github/workflows/e2e-report.yml @@ -0,0 +1,71 @@ +name: E2E report + +# A weekly end-to-end run with a report, so breakage from new WordPress or +# browser releases shows up even in weeks without commits. +on: + schedule: + # Fridays at 12:00 UTC. GitHub schedules always run in UTC: for 12:00 + # West Africa Time (UTC+1) use '0 11 * * 5'. Schedules only run on the + # default branch. + - cron: '0 12 * * 5' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: e2e-report + cancel-in-progress: false + +jobs: + e2e-report: + name: End-to-end report + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '7.4' + coverage: none + - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + composer-options: '--no-dev' + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci --ignore-scripts + - run: npm run build:admin + - run: npm run test:e2e:install + - run: npm run env:start + + # Keep going when tests fail so the report is still published. + - name: Run end-to-end tests + id: e2e + continue-on-error: true + run: npm run test:e2e + + - name: Upload report + id: report + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: e2e-report-${{ github.run_number }} + path: | + playwright-report + test-reports + test-results + retention-days: 30 + if-no-files-found: warn + + - name: Write the job summary + if: always() + run: node tests/e2e/report/summary.js test-reports/e2e-results.json "${{ steps.report.outputs.artifact-url }}" >> "$GITHUB_STEP_SUMMARY" + + # Mark the run red after the report is out, so failures still notify. + - name: Fail when tests failed + if: steps.e2e.outcome == 'failure' + run: | + echo "::error::End-to-end tests failed. See the job summary and the e2e-report-${{ github.run_number }} artifact." + exit 1 diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml index 2f9cce7..6730153 100644 --- a/.github/workflows/publish-package.yml +++ b/.github/workflows/publish-package.yml @@ -2,6 +2,9 @@ name: Publish to WordPress.org on: release: types: [released] +permissions: + contents: read + jobs: tag: name: New Release @@ -9,27 +12,28 @@ jobs: env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} steps: - - uses: act10ns/slack@v2 + - uses: act10ns/slack@d96404edccc6d6467fc7f8134a420c851b1e9054 # v2 with: status: starting if: ${{ always() }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Node.js environment - uses: actions/setup-node@v2.5.2 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version-file: .nvmrc + node-version-file: .nvmrc + cache: npm - name: Build Plugin uses: ./.github/actions/make-build - name: WordPress Plugin Deploy id: deploy - uses: 10up/action-wordpress-plugin-deploy@stable + uses: 10up/action-wordpress-plugin-deploy@54bd289b8525fd23a5c365ec369185f2966529c2 # stable env: SVN_USERNAME: ${{ secrets.SVN_USERNAME }} SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} SLUG: rave-payment-forms - name: push build status to Slack - uses: 8398a7/action-slack@v3 + uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e # v3 with: status: ${{ job.status }} fields: repo,message,commit,author,action,eventName,ref,workflow,job,took,pullRequest diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index b46d569..600d2e6 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -7,6 +7,9 @@ on: types: - opened +permissions: + contents: read + jobs: code-check: runs-on: ubuntu-latest @@ -14,10 +17,9 @@ jobs: OS: ubuntu-latest steps: - name: checkout code - uses: actions/checkout@v2 - + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Checkmarx One ClI Action - uses: checkmarx/ast-github-action@main + uses: checkmarx/ast-github-action@882c03f380f4bdddae2d068540cbd73efe0796ac # main with: project_name: WordPress cx_tenant: Flutterwave diff --git a/.gitignore b/.gitignore index 0401cc4..5f69b16 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,11 @@ build/ .docker/bin .docker/logs .docker/data -.docker/wordpress \ No newline at end of file +.docker/wordpress +# Tests +/.phpunit.cache +/coverage +/playwright-report +/test-results +/tests/e2e/.state +/test-reports diff --git a/.npmpackagejsonlintrc.json b/.npmpackagejsonlintrc.json new file mode 100644 index 0000000..627a598 --- /dev/null +++ b/.npmpackagejsonlintrc.json @@ -0,0 +1,11 @@ +{ + "extends": "@wordpress/npm-package-json-lint-config", + "rules": { + "valid-values-license": [ + "error", + [ + "MIT" + ] + ] + } +} diff --git a/.nvmrc b/.nvmrc index 789d3c1..a45fd52 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.18.0 \ No newline at end of file +24 diff --git a/.prettierrc.js b/.prettierrc.js index e596d24..ca2665d 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1 +1 @@ -module.exports = require('@wordpress/prettier-config'); +module.exports = require( '@wordpress/prettier-config' ); diff --git a/.wordpress-org/screenshot-1.png b/.wordpress-org/screenshot-1.png index 00b327f..e5b6921 100644 Binary files a/.wordpress-org/screenshot-1.png and b/.wordpress-org/screenshot-1.png differ diff --git a/.wordpress-org/screenshot-2.png b/.wordpress-org/screenshot-2.png index 83fe959..4ea1d7b 100644 Binary files a/.wordpress-org/screenshot-2.png and b/.wordpress-org/screenshot-2.png differ diff --git a/.wordpress-org/screenshot-3.png b/.wordpress-org/screenshot-3.png index 6be7cbc..713aeb9 100644 Binary files a/.wordpress-org/screenshot-3.png and b/.wordpress-org/screenshot-3.png differ diff --git a/.wordpress-org/screenshot-4.png b/.wordpress-org/screenshot-4.png index 0c7d830..e81c3d1 100644 Binary files a/.wordpress-org/screenshot-4.png and b/.wordpress-org/screenshot-4.png differ diff --git a/.wordpress-org/screenshot-5.png b/.wordpress-org/screenshot-5.png index b2629a3..cae39b3 100644 Binary files a/.wordpress-org/screenshot-5.png and b/.wordpress-org/screenshot-5.png differ diff --git a/.wordpress-org/screenshot-6.png b/.wordpress-org/screenshot-6.png index fa8beeb..648c926 100644 Binary files a/.wordpress-org/screenshot-6.png and b/.wordpress-org/screenshot-6.png differ diff --git a/.wordpress-org/screenshot-7.png b/.wordpress-org/screenshot-7.png index 93337aa..b4366a5 100644 Binary files a/.wordpress-org/screenshot-7.png and b/.wordpress-org/screenshot-7.png differ diff --git a/.wp-env.json b/.wp-env.json index ddd7167..dba07cf 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,10 +1,21 @@ { - "core": "https://wordpress.org/latest.zip", - "phpVersion": "7.4", - "plugins": ["rave-payment-forms.zip"], - "port": 1000, - "testsPort": 1001, + "$schema": "https://schemas.wp.org/trunk/wp-env.json", + "core": null, + "phpVersion": "8.3", + "plugins": [ + "https://downloads.wordpress.org/plugin/easy-digital-downloads.3.7.1.zip", + "https://downloads.wordpress.org/plugin/give.4.17.0.zip" + ], + "mappings": { + "wp-content/plugins/rave-payment-forms": ".", + "wp-content/mu-plugins/flw-e2e-mock.php": "./tests/e2e/fixtures/mock-flutterwave.php" + }, "config": { - "WP_DEBUG_DISPLAY": false + "WP_DEBUG": true, + "WP_DEBUG_DISPLAY": false, + "SCRIPT_DEBUG": true + }, + "lifecycleScripts": { + "afterStart": "wp-env run cli wp plugin activate rave-payment-forms easy-digital-downloads give && wp-env run tests-cli wp plugin activate rave-payment-forms" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index aab1fd4..451e737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,53 @@ # Changelog +## 1.1.0 | 29-09-2026 + +### Added +- New onboarding flow for first-time setup (welcome, general details, API & webhook, payment methods, redirects), matching the Flutterwave WooCommerce plugin. +- Tabbed settings screen replacing the old settings form, with a generated webhook secret hash, copyable webhook URL and test/live key detection. +- Payment methods are chosen from a list (cards, stablecoin, bank transfer, mobile money, Apple Pay, Google Pay, Opay, plus advanced options) and enforced at checkout. +- Donation form heading and message are now shown on the form. +- Blocks: Payment Button, Payment Form, Donation Form, Pricing Card and Accepted Payment Methods, with per-block colours and corner radius. +- Payment form shortcode: `heading`, `description`, `layout="compact"`, `width="full"` and `show_secured="0"` attributes, `exclude="fullname"`, and `{amount}` in the button text. +- Donation form shortcode: `heading`, `message`, `currency`, `amounts` (suggested amounts) and `show_frequency="0"` attributes. +- Integration analytics through the Flutterwave SigNoz service (app.created, request.sent, app.transaction, app.error), ported from the WooCommerce plugin. Events are sent in the background and can be turned off with `FLW_DISABLE_TELEMETRY` or the `flw_signoz_enabled` filter. +- Payment Forms screen listing every Flutterwave block and shortcode in use, with the page it is on, its status and the payments it has taken, plus a builder that creates a draft page or copies the shortcode with a live preview. +- Payments now record the page they were made on. +- Guided tips: a short tour of each admin screen the first time a merchant opens it after onboarding, with a Help menu on every screen to replay a tour or turn tips on and off (saved per user). +- Integrations screen: install and activate the Flutterwave WooCommerce extension, switch on the new Easy Digital Downloads and GiveWP gateways (paid on Flutterwave's hosted checkout, confirmed on return and by webhook), and see what is coming soon. +- Custom CSV download of transactions for a date range, with spreadsheet formula injection neutralised. + +### Removed +- The unused third-party integrations scaffolding (exchange-rate service sample and its admin page) is no longer loaded or shipped. + +### Changed +- Payment and donation forms use the new Flutterwave design +- Transactions screen redesigned: customer, amount, date and status table with date-range, status and currency filters, pagination and a details drawer to re-verify or delete a record., with self-hosted fonts. +- The payment page title, description and logo settings are now sent to Flutterwave checkout. +- Form styles only load on pages that contain a form. + +### Security +- [FIXED] Payment amount and currency are enforced server-side through a signed form configuration. +- [FIXED] Transaction verification checks the stored record's tx_ref, amount and currency instead of transaction metadata. +- [FIXED] Stored XSS through shortcode attributes and in the transactions list. +- [FIXED] Webhooks are rejected when no secret hash is configured; hashes are compared in constant time. +- [FIXED] `update-transaction` requires `manage_options` and a REST nonce. +- [ADDED] Easy Digital Downloads and GiveWP payments are only changed by a transaction verified with Flutterwave or the signed webhook, never by the return URL's query arguments, and completion is locked so a simultaneous return and webhook cannot complete an order twice. +- [ADDED] Checkout rate limiting (`flw_checkout_rate_limit` filter) and validated settings. +- [CHANGED] Secret key and secret hash fields are masked. + +### Fixed +- Checkout showed nothing when Flutterwave rejected the payment request; it now shows an error. +- The plugin version constant said 1.0.6 while the plugin header said 1.0.7. +- Name fields were never rendered on the payment form (and `split_name` never showed first/last name). +- Editors saw the "API keys missing" notice instead of the form on configured sites. +- Translation loading was triggered too early on WordPress 6.7+. +- Checkout errors were not shown to customers. + +### Development +- Dependencies upgraded; `npm audit` and `composer audit` report no vulnerabilities. +- New test suites: unit (PHPUnit + Brain Monkey), integration (WordPress test suite via wp-env) and end-to-end (Playwright). +- WordPress Coding Standards 3, ESLint flat config, Node.js 24, GitHub Actions pinned to commit SHAs. + ## 1.0.7 | 29-08-2023 Update admin settings view for Transactions. diff --git a/Makefile b/Makefile index 837259a..656c518 100644 --- a/Makefile +++ b/Makefile @@ -13,17 +13,26 @@ down: docker-compose -f .docker/docker-compose.yml down -dev-js: - npm run start +env-start: + npm run env:start -build-production-js: - npm run preuglify && npm run uglify +env-stop: + npm run env:stop + +test: + npm test + +test-unit: + npm run test:unit -build-production-docs: - npm run docs:build +lint: + npm run lint -dev-docs: - npm run docs:dev +audit: + npm run audit + +build-production-js: + npm run preuglify && npm run uglify wp-format: npm run format @@ -35,7 +44,7 @@ zip: rm rave-payment-forms.zip && npm run plugin-zip inspection: - ./vendor/bin/phpcs -p . --standard=PHPCompatibilityWP + composer run phpcompatibility:check build: npm run build diff --git a/assets/css/admin/integrations.css b/assets/css/admin/integrations.css index 9870008..7f5b48d 100644 --- a/assets/css/admin/integrations.css +++ b/assets/css/admin/integrations.css @@ -1,25 +1,25 @@ #wpbody { - display: inline-block; - width: 100%; - padding-top: 0; - margin-top: 60px; + display: inline-block; + width: 100%; + padding-top: 0; + margin-top: 60px; } -#wpbody{ - box-sizing: border-box; +#wpbody { + box-sizing: border-box; } .flw__integration { - background-color: #FFF; - width: 100%; - padding: 30px; - display: flex; - flex-direction: column; - margin: 0; + background-color: #fff; + width: 100%; + padding: 30px; + display: flex; + flex-direction: column; + margin: 0; } .flutterwave-page .wrap { - margin: 10px 20px 0 2px; + margin: 10px 20px 0 2px; } diff --git a/assets/css/admin/integrations.scss b/assets/css/admin/integrations.scss index f325a55..2d68391 100644 --- a/assets/css/admin/integrations.scss +++ b/assets/css/admin/integrations.scss @@ -1,6 +1,6 @@ .flw__integration { - background-color: #FFF; - width: 100%; - margin: auto; + background-color: #fff; + width: 100%; + margin: auto; } diff --git a/assets/css/flw-donation.css b/assets/css/flw-donation.css deleted file mode 100644 index 39bf902..0000000 --- a/assets/css/flw-donation.css +++ /dev/null @@ -1,495 +0,0 @@ -h1, h2, h3, h4, h5, h6, p { - margin: 0; -} - -html { - box-sizing: border-box; -} - -*, *::after, *::before { - box-sizing: inherit; -} - -body { - font-size: 14px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; - font-weight: 400; - color: #212b36; - line-height: 1.5; - letter-spacing: 0.4px; - background-color: #F4F6F8; -} - -body { - margin: 0; -} - -a:active, a:hover { - outline-width: 0; -} - -.contact{ - padding: 0 30px 0 30px; - height: 100vh; - overflow: scroll; - - - -} - - -.donation__section__left__header { - margin-top: 50px; - color: #f4f6f8; - font-size: 2.5em; -} - - -.donation__section__left__description { - margin-top: 30px; - width: 500px; - line-height: normal; - font-size: 20px; -} - -.contact .donation-form { - width: 360px; - max-width: 100%; -} - -.contact .donation-form .radio-group { - margin-bottom: 20px; - width: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.form__item--marginBottom-12 { - margin-bottom: 12px; -} - -.form__item { - margin-bottom: 20px; -} - -@media (max-width: 480px){ - - .contact .donation-form .radio-group .radio-box-group--left { - font-size: 12px; - } -} - -.contact .donation-form .radio-group .radio-box-group--left { - position: relative; -} - - -.contact .donation-form .radio-group input[type="radio"] { - z-index: 100; - display: none; -} - -.contact .donation-form .radio-group input[type="radio"]:checked + label { - background: #ffffff; - color: #12122c; -} - -@media (max-width: 480px){ - - .contact .donation-form .radio-group label { - display: block; - text-align: center; - padding: 5px 20px; - font-size: 10px; - } - -} - -.contact .donation-form .radio-group label { - padding: 10px 40px; - text-transform: uppercase; - border: 1px solid #f4f6f8; - font-weight: 600; - color: #fff; -} - - - -@media (max-width: 991px){ - .contact .donation-form { - width: 360px; - max-width: 90%; - margin: 0 auto; - } -} - -@media (max-width: 480px){ - .donation .donation-form .radio-group .radio-box-group--right { - font-size: 12px; - } -} - -.donation .donation-form .radio-group .radio-box-group--right { - position: relative; -} - -span.phone{ - /* float: right; - padding: 2em; */ - display: flex; - align-items: center; - padding: 0 100px; - height: 50px; - - color: black; - text-decoration: none; - font-weight: 600; - font-size: 12px; -} - -.contact_top { - display: flex; - align-items: center; - padding: 0 100px; - height: 50px; -} - -@media (max-width: 991px){ - - .contact_top_left a { - font-size: 10px; - } - -} - -.contact_top_left a { - text-decoration: none; - color: black; - font-weight: 600; - font-size: 12px; -} - -.contact__top { - display: flex; - align-items: center; - padding: 0 100px; - height: 50px; -} - - - -@media (max-width: 991px){ - .contact__top { - padding: 15px 0; - } -} - - - -.contact_top_right{ - margin-left: auto; -} - -.contact_top_right a { - color: black; - text-decoration: none; - font-weight: 600; - font-size: 12px; -} - -@media (max-width: 991px){ - .contact__top__right a { - font-size: 10px; - } -} - - - -@media (max-width: 991px){ - span.phone { - padding: 15px 0; - font-size: 10px; - } -} - - - - - -.wrapper{ - background-image: linear-gradient(rgba(18, 18, 44, 0.8), rgba(18, 18, 44, 0.8)), ; -} - -@media (max-width: 480px){ - - .wrapper { - overflow: scroll; - } - -} - -.wrapper{ - position: relative; - background-size: cover; - height: calc(100vh - 90px); -} - - -.form_bottom{ - position: relative; - /* background: url(/static/img/donation-bg@3x.png) center no-repeat; */ - background-size: cover; - height: calc(100vh - 90px); -} - -.form__input { - display: block; - width: 100%; - padding: 10px 12px; - font-size: 14px; - line-height: 1.43; - color: inherit; - font-family: inherit; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #c4cdd5; - border-radius: 4px; - outline: none; - resize: none; - background-color: #ffffff; - box-shadow: 0 1px 0 0 rgb(22 29 37 / 5%); - transition: border-color ease-in-out .3s, box-shadow ease-in-out .3s; -} - -.contact .donation-form .form__inner { - background-color: white; - padding: 20px; -} - -.textCenter { - text-align: center; -} - -.form__item--noBottomMargin { - margin-bottom: 0; -} - -.newObject { - display: flex; -} - -.newObject__select { - width: 30%; -} - -.select { - position: relative; -} - -.contact .donation-form .form__inner .select__input { - color: #212b36; -} - -.select__input:disabled { - background-color: #f4f7fc; - color: #637381; -} - -.select__input--noRightBorderRadius { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -.select__icon { - position: absolute; - right: 11px; - top: 8px; - pointer-events: none; - padding-left: 3px; - background-color: #ffffff; -} - -svg:not(:root) { - overflow: hidden; -} - -.newObject__input { - width: 70%; -} - - - -.form__input[readonly] { - background-color: #f4f7fc; - color: #637381; -} - -.form__input--noLeftBorderRadius { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left: 0; -} - -button, input { - overflow: visible; -} - -button, input, optgroup, select, textarea { - font-family: sans-serif; - font-size: 100%; - line-height: 1.15; - margin: 0; -} - -.select__input { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -/* padding: 16px 12px; */ - font-size: 14px; - line-height: 1.43; - color: inherit; - display: block; - width: 100%; - font-family: inherit; - border: 1px solid #c4cdd5; - border-radius: 4px; - outline: none; - background-color: #ffffff; - box-shadow: 0 1px 0 0 rgb(22 29 37 / 5%); - transition: border-color ease-in-out .3s, box-shadow ease-in-out .3s; -} - -.donation__section__right__badge { - margin: 40px auto; - display: flex; -} - -.secured__badge { - margin-bottom: 6px; - display: flex; - justify-content: center; - align-items: center; -} - -.secured__badge__container { - display: flex; - border-radius: 3px; - padding: 6px 12px; - justify-content: center; - align-items: center; - background-color: #000000; -} - -.secured__badge__icon { - padding-right: 5px; -} - -.secured__badge__text { - color: #f5a623; - font-weight: 600; - font-size: 10px; -} - -.donation__section { - display: flex; - height: calc(100vh - 90px); -} - -@media (max-width: 991px){ - - .donation__section__left { - display: none; - } - -} - - - -.donation__section__left { - width: 60%; - padding: 50px 100px; - color: #ffffff; -} - -.donation__section__right { - width: 40%; - padding: 80px 30px; -} - -@media (max-width: 991px){ - - .donation__section__right { - width: 100%; - padding: 20px 20px; - } - -} - -.text-uppercase { - text-transform: uppercase; -} - -.btn { - color: inherit; - display: inline-block; - font-weight: 600; - line-height: 1.71; - text-align: center; - white-space: nowrap; - vertical-align: middle; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - border: none; - border-radius: 4px; - padding: 8px 16px; - font-size: 14px; - text-decoration: none; - transition: all 0.3s ease-in-out; - font-family: inherit; - outline: none; - box-shadow: 1px 1px 3px 0 rgb(50 50 54 / 13%), 0 0 3px 1px rgb(63 63 68 / 5%); -} - -.btn--pay { - padding: 8px 32px; -} - -.btn--block { - display: block; - width: 100%; -} - -.btn--primary { - color: #fff; - background-color: #F5A623; -} - - - -button, html [type="button"], [type="reset"], [type="submit"] { - -webkit-appearance: button; -} - -@media screen and (max-width: 767px){ - - .hidden--xs { - display: none !important; - } -} - - -@media screen and (min-width: 768px){ - - .show--xs { - display: none !important; - } -} - - - - diff --git a/assets/css/flw-fonts.css b/assets/css/flw-fonts.css new file mode 100644 index 0000000..9661c9b --- /dev/null +++ b/assets/css/flw-fonts.css @@ -0,0 +1,108 @@ +/** + * Self-hosted Flutterwave webfonts, shared by the admin app and the + * payment forms. + * + * Served from the plugin rather than a font CDN: no third-party requests + * (which keeps privacy reviews simple) and the files come from the same + * origin as the page. + * Millik ships a single 400 weight, so headings must not request bold. + */ + +@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/assets/css/flw.css b/assets/css/flw.css index 4256c41..6915ecf 100644 --- a/assets/css/flw.css +++ b/assets/css/flw.css @@ -1,164 +1,628 @@ -.flw-mssing-api-keys { - color: #ff9b00; - font-size: 1.5em; - padding: .5em; - background: #f4f6f8; - text-align: center; +/** + * Flutterwave payment and donation forms. + * + * Shares its tokens with the admin app (client/admin/admin.scss) so a form + * looks like the onboarding screens that created it. Selectors use the form's + * own classes so theme styles for bare inputs and buttons do not win. + */ + +.flutterwave-payment-form, +.flutterwave-donation-form { + + /* Blocks override these through --flw-accent, --flw-accent-text + and --flw-custom-radius. */ + --flw-orange: var(--flw-accent, #ff9b00); + --flw-on-orange: var(--flw-accent-text, #1a1a1a); + --flw-ink: #101828; + --flw-body: #475467; + --flw-muted: #98a2b3; + --flw-border: #d0d5dd; + --flw-line: #eaecf0; + --flw-radius: var(--flw-custom-radius, 8px); + --flw-sans: "Moderat", "Inter", -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif; + --flw-serif: "Millik", "Iowan Old Style", "Palatino Linotype", palatino, georgia, serif; + + background: #fff; + border: 1px solid var(--flw-line); + border-radius: 12px; + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 12px 32px -12px rgba(16, 24, 40, 0.12); + box-sizing: border-box; + color: var(--flw-ink); + font-family: var(--flw-sans); + font-size: 15px; + line-height: 1.5; + margin: 24px auto; + max-width: var(--flw-max-width, 440px); + padding: 32px; + text-align: left; + width: 100%; } -.flutterwave-overlay { - position: fixed; /* Sit on top of the page content */ - width: 100%; /* Full width (cover the whole page) */ - height: 100%; /* Full height (cover the whole page) */ - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0,0,0,0.5); /* Black background with opacity */ - z-index: 2; /* Specify a stack order in case you're using a different order for other elements */ - cursor: pointer; /* Add a pointer on hover */ +.flutterwave-payment-form *, +.flutterwave-payment-form *::before, +.flutterwave-payment-form *::after, +.flutterwave-donation-form *, +.flutterwave-donation-form *::before, +.flutterwave-donation-form *::after { + box-sizing: inherit; } -.flw-overlay-text { - position: absolute; - top: 50%; - left: 50%; - font-size: 50px; - color: white; - transform: translate(-50%,-50%); - -ms-transform: translate(-50%,-50%); -} - -.flutterwave-payment-form, .flutterwave-donation-form { - /*position: absolute;*/ - background: #FFFFFF; - border: 1px solid rgba(18, 18, 44, 0.1); - box-shadow: 2px 6px 20px rgba(18, 18, 44, 0.07); - border-radius: 8px; - padding: 30px; - width: 400px; - font-family: "Source Sans Pro","HelveticaNeue-Light","Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif; - margin: auto; -} - -.flutterwave-payment-form > form, .flutterwave-donation-form > form { - width: 100%; - display: flex; - flex-direction: column; - justify-content: center; +.flutterwave-payment-form form, +.flutterwave-donation-form form { + display: flex; + flex-direction: column; + margin: 0; + width: 100%; +} + +/* Header ------------------------------------------------------------------ */ + +.flutterwave-payment-form .flw-form-header, +.flutterwave-donation-form .flw-form-header { + margin-bottom: 24px; +} + +.flutterwave-payment-form .flw-form-title, +.flutterwave-donation-form .flw-form-title { + color: var(--flw-ink); + font-family: var(--flw-serif); + font-size: 26px; + font-weight: 400; + letter-spacing: -0.01em; + line-height: 1.2; + margin: 0 0 6px; +} + +.flutterwave-payment-form .flw-form-description, +.flutterwave-donation-form .flw-form-description { + color: var(--flw-body); + margin: 0; +} + +/* Fields ------------------------------------------------------------------ */ + +.flutterwave-payment-form label, +.flutterwave-donation-form label { + color: var(--flw-ink); + display: block; + font-size: 14px; + font-weight: 500; + line-height: 1.4; + margin: 0 0 6px; +} + +.flutterwave-payment-form .flw-form-input-text, +.flutterwave-payment-form .flw-form-select, +.flutterwave-donation-form .flw-form-input-text, +.flutterwave-donation-form .flw-form-select { + appearance: none; + background-color: #fff; + border: 1px solid var(--flw-border); + border-radius: var(--flw-radius); + box-shadow: none; + color: var(--flw-ink); + display: block; + font-family: inherit; + font-size: 15px; + height: auto; + line-height: 1.4; + margin: 0 0 18px; + max-width: none; + min-height: 48px; + padding: 12px 14px; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + width: 100%; +} + +.flutterwave-payment-form .flw-form-select, +.flutterwave-donation-form .flw-form-select { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m4 6 4 4 4-4' fill='none' stroke='%23475467' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-position: right 14px center; + background-repeat: no-repeat; + background-size: 16px; + cursor: pointer; + padding-right: 42px; +} + +.flutterwave-payment-form .flw-form-input-text::placeholder, +.flutterwave-donation-form .flw-form-input-text::placeholder { + color: var(--flw-muted); + opacity: 1; +} + +.flutterwave-payment-form .flw-form-input-text:hover, +.flutterwave-payment-form .flw-form-select:hover, +.flutterwave-donation-form .flw-form-input-text:hover, +.flutterwave-donation-form .flw-form-select:hover { + border-color: var(--flw-muted); +} + +.flutterwave-payment-form .flw-form-input-text:focus, +.flutterwave-payment-form .flw-form-select:focus, +.flutterwave-donation-form .flw-form-input-text:focus, +.flutterwave-donation-form .flw-form-select:focus { + border-color: var(--flw-orange); + box-shadow: 0 0 0 3px rgba(255, 155, 0, 0.18); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--flw-orange) 22%, transparent); + outline: none; +} + +/* Amount summary ---------------------------------------------------------- */ + +.flutterwave-payment-form .flw_payment_overview { + align-items: center; + background: #fff6e9; + background: color-mix(in srgb, var(--flw-orange) 9%, #fff); + border-radius: var(--flw-radius); + display: flex; + gap: 12px; + justify-content: space-between; + margin: 0 0 20px; + padding: 14px 16px; +} + +.flutterwave-payment-form .flw_total_label { + color: var(--flw-body); + font-size: 14px; + white-space: nowrap; +} + +.flutterwave-payment-form .flw_amount_to_pay { + color: var(--flw-ink); + font-family: var(--flw-serif); + font-size: clamp(18px, 5vw, 24px); + line-height: 1.2; + white-space: nowrap; +} + +/* Button ------------------------------------------------------------------ */ + +.flutterwave-payment-form .flw-pay-now-button, +.flutterwave-donation-form .flw-pay-now-button { + align-items: center; + background: var(--flw-orange); + border: 0; + border-radius: var(--flw-radius); + box-shadow: none; + color: var(--flw-on-orange); + cursor: pointer; + display: flex; + font-family: inherit; + font-size: 16px; + font-weight: 500; + gap: 10px; + justify-content: center; + letter-spacing: normal; + line-height: 1.2; + margin: 6px 0 0; + min-height: 50px; + padding: 0 24px; + text-shadow: none; + text-transform: none; + transition: background-color 0.15s ease; + width: 100%; +} + +.flutterwave-payment-form .flw-pay-now-button:hover:not(:disabled), +.flutterwave-donation-form .flw-pay-now-button:hover:not(:disabled) { + background: var(--flw-orange); + color: var(--flw-on-orange); + filter: brightness(0.93); +} + +.flutterwave-payment-form .flw-pay-now-button:focus-visible, +.flutterwave-donation-form .flw-pay-now-button:focus-visible { + box-shadow: 0 0 0 3px rgba(255, 155, 0, 0.35); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--flw-orange) 40%, transparent); + outline: none; +} + +.flutterwave-payment-form .flw-pay-now-button:disabled, +.flutterwave-donation-form .flw-pay-now-button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +/* Messages ---------------------------------------------------------------- */ + +.flutterwave-payment-form .flw-error, +.flutterwave-donation-form .flw-error { + background: #fef3f2; + border: 1px solid #fecdca; + border-radius: var(--flw-radius); + color: #b42318; + display: none; + font-size: 14px; + margin: 0 0 18px; + padding: 10px 14px; +} + +.flutterwave-payment-form .flw-error.is-visible, +.flutterwave-donation-form .flw-error.is-visible { + display: block; +} + +.flutterwave-payment-form #notice:empty, +.flutterwave-donation-form #notice:empty { + display: none; +} + +.flutterwave-payment-form .flw-secured, +.flutterwave-donation-form .flw-secured { + align-items: center; + color: var(--flw-muted); + display: flex; + font-size: 12px; + gap: 6px; + justify-content: center; + margin: 18px 0 0; +} + +.flutterwave-payment-form .flw-secured strong, +.flutterwave-donation-form .flw-secured strong { + color: var(--flw-body); + font-weight: 500; +} + +/* Donation amount presets ------------------------------------------------ */ + +.flutterwave-donation-form .flw-amount-presets { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 0 0 12px; +} + +.flutterwave-donation-form .flw-amount-preset { + background: #fff; + border: 1px solid var(--flw-border); + border-radius: 999px; + box-shadow: none; + color: var(--flw-ink); + cursor: pointer; + font-family: inherit; + font-size: 14px; + font-weight: 500; + line-height: 1.2; + margin: 0; + padding: 9px 16px; + text-transform: none; +} + +.flutterwave-donation-form .flw-amount-preset:hover { + border-color: var(--flw-muted); +} + +.flutterwave-donation-form .flw-amount-preset.is-selected { + background: #fff6e9; + background: color-mix(in srgb, var(--flw-orange) 12%, #fff); + border-color: var(--flw-orange); +} + +/* Compact layout (payment button, pricing card) ---------------------------- */ + +.flutterwave-payment-form.flw-layout-compact { + background: transparent; + border: 0; + border-radius: 0; + box-shadow: none; + margin: 0; + max-width: none; + padding: 0; +} + +.flutterwave-payment-form.flw-layout-compact form { + align-items: stretch; + flex-flow: row wrap; + gap: 8px; +} + +.flutterwave-payment-form.flw-layout-compact label, +.flutterwave-payment-form.flw-layout-compact .flw_payment_overview { + border: 0; + clip-path: inset(50%); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.flutterwave-payment-form.flw-layout-compact .flw-form-input-text, +.flutterwave-payment-form.flw-layout-compact .flw-form-select { + flex: 1 1 200px; + margin: 0; + width: auto; +} + +.flutterwave-payment-form.flw-layout-compact .flw-pay-now-button { + flex: 0 0 auto; + margin: 0; + width: auto; +} + +.flutterwave-payment-form.flw-layout-compact.flw-width-full form { + flex-direction: column; +} + +.flutterwave-payment-form.flw-layout-compact.flw-width-full .flw-form-input-text, +.flutterwave-payment-form.flw-layout-compact.flw-width-full .flw-form-select { + + /* In a column the 200px basis would become the field's height. */ + flex: 0 0 auto; + width: 100%; +} + +.flutterwave-payment-form.flw-layout-compact.flw-width-full .flw-pay-now-button { + width: 100%; +} + +.flutterwave-payment-form.flw-layout-compact .flw-secured { + justify-content: flex-start; + margin-top: 10px; +} + +.flutterwave-payment-form.flw-layout-compact.flw-width-full .flw-secured { + justify-content: center; +} + +/* Pricing card ------------------------------------------------------------ */ + +.flw-pricing-card { + --flw-card-accent: var(--flw-accent, #ff9b00); + + background: #fff; + border: 1px solid #eaecf0; + border-radius: 12px; + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 12px 32px -12px rgba(16, 24, 40, 0.12); + box-sizing: border-box; + color: #101828; + display: flex; + flex-direction: column; + font-family: Moderat, Inter, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, sans-serif; + font-size: 15px; + line-height: 1.5; + margin: 24px auto; + max-width: 360px; + padding: 28px; + position: relative; + width: 100%; +} + +.flw-pricing-card.is-highlighted { + border: 2px solid var(--flw-card-accent); +} + +.flw-pricing-card__badge { + background: var(--flw-card-accent); + border-radius: 999px; + color: var(--flw-accent-text, #1a1a1a); + font-size: 12px; + font-weight: 500; + padding: 4px 10px; + position: absolute; + right: 20px; + top: -12px; +} + +.flw-pricing-card__name { + color: #475467; + font-size: 15px; + font-weight: 500; + margin: 0 0 8px; +} + +.flw-pricing-card__price { + align-items: baseline; + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 0 0 12px; +} + +.flw-pricing-card__amount { + font-family: Millik, "Iowan Old Style", "Palatino Linotype", palatino, georgia, serif; + font-size: 38px; + line-height: 1.1; +} + +.flw-pricing-card__period { + color: #98a2b3; + font-size: 14px; +} + +.flw-pricing-card__description { + color: #475467; + margin: 0 0 16px; +} + +.flw-pricing-card__features { + border-top: 1px solid #eaecf0; + list-style: none; + margin: 0 0 24px; + padding: 16px 0 0; +} + +.flw-pricing-card__features li { + align-items: flex-start; + display: flex; + gap: 10px; + margin: 0 0 10px; +} + +.flw-pricing-card__features svg { + color: var(--flw-card-accent); + flex: 0 0 auto; + margin-top: 4px; } -.flutterwave-payment-form label, .flutterwave-donation-form label { - /*font-family: Inter;*/ - font-size: 16px; - font-weight: 400; - line-height: 20px; - letter-spacing: 0px; - text-align: left; +.flw-pricing-card .flutterwave-payment-form { + margin-top: auto; } -.flutterwave-payment-form ::placeholder, .flutterwave-donation-form ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ - font-size: 16px; - font-weight: 400; - line-height: 20px; - letter-spacing: 0px; - text-align: left; - opacity: 1; /* Firefox */ -} - -.flutterwave-payment-form :-ms-input-placeholder, .flutterwave-donation-form :-ms-input-placeholder { /* Internet Explorer 10-11 */ - font-size: 16px; - font-weight: 400; - line-height: 20px; - letter-spacing: 0px; - text-align: left; -} - -.flutterwave-payment-form ::-ms-input-placeholder, .flutterwave-donation-form ::-ms-input-placeholder { /* Microsoft Edge */ - font-size: 16px; - font-weight: 400; - line-height: 20px; - letter-spacing: 0px; - text-align: left; -} - -.flutterwave-payment-form input[type="text"], -.flutterwave-payment-form input[type="number"], -.flutterwave-payment-form input[type="tel"], -.flutterwave-payment-form input[type="email"], .flutterwave-payment-form select { - background: #FFFFFF; - border: 1px solid #BDBDBD; - /*border: 1px solid #F5A623;*/ - border-radius: 4px; - padding: 15px; - font-size: 16px; - font-weight: 400; - display: flex; - justify-content: flex-start; - margin-bottom: .9em; -} - - -#flw-first-name:hover, #flw-customer-email:hover, #flw-last-name:hover, #flw-amount:hover, #flw-full-name:hover, #flw-phone:hover{ - border: 1px solid #F5A623; -} - -.flutterwave-payment-form form #flw-pay-now-button, .flutterwave-donation-form form #flw-pay-now-button { - background: #F5A623; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 56px; - color: #000000; - border-radius: 4px; -} - -.flutterwave-payment-form form #flw-pay-now-button:disabled, .flutterwave-donation-form form #flw-pay-now-button:disabled { - background-color: rgba(239, 239, 239, 0.3); - color: rgba(16, 16, 16, 0.3); - border-color: rgba(118, 118, 118, 0.3); -} - -.flutterwave-payment-form form #flw-pay-now-button:hover, .flutterwave-donation-form form #flw-pay-now-button:hover { - background: #6672c7; - color: #FFFFFF; +/* Accepted payment methods ------------------------------------------------ */ + +.flw-payment-methods { + color: #475467; + font-family: Moderat, Inter, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, sans-serif; + font-size: 14px; +} + +.flw-payment-methods__label { + color: #101828; + font-weight: 500; + margin: 0 0 8px; +} + +.flw-payment-methods__list { + display: flex; + flex-wrap: wrap; + gap: 8px; + list-style: none; + margin: 0; + padding: 0; +} + +.flw-payment-methods__item { + align-items: center; + background: #fff; + border: 1px solid #eaecf0; + border-radius: 999px; + color: #101828; + display: inline-flex; + font-size: 13px; + gap: 6px; + margin: 0; + padding: 6px 12px; +} + +.flw-payment-methods__item svg { + color: #475467; +} + +.flw-payment-methods .flw-secured { + align-items: center; + color: #98a2b3; + display: flex; + font-size: 12px; + gap: 6px; + margin: 12px 0 0; +} + +.flw-payment-methods .flw-secured strong { + color: #475467; + font-weight: 500; +} + +/* Block alignment --------------------------------------------------------- */ + +.wp-block-flutterwave-payment-form.alignleft > *, +.wp-block-flutterwave-donation-form.alignleft > *, +.wp-block-flutterwave-pricing-card.alignleft > * { + margin-left: 0; +} + +.wp-block-flutterwave-payment-form.alignright > *, +.wp-block-flutterwave-donation-form.alignright > *, +.wp-block-flutterwave-pricing-card.alignright > * { + margin-right: 0; } -.flutterwave-payment-form form > div.flw_payment_overview { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; +.wp-block-flutterwave-payment-button.aligncenter .flw-layout-compact form, +.wp-block-flutterwave-payment-methods.aligncenter .flw-payment-methods__list { + justify-content: center; } -.flutterwave-payment-form form > div.flw_payment_overview > div.flw_total_label { - font-size: 16px; - font-weight: 400; +.wp-block-flutterwave-payment-methods.aligncenter { + text-align: center; } -.flutterwave-payment-form form > div.flw_payment_overview > div.flw_amount_to_pay { - font-size: 22px; - font-weight: 600; - color: #35a162; +.wp-block-flutterwave-payment-methods.aligncenter .flw-secured { + justify-content: center; } -.flutterwave-payment-form div, .flutterwave-donation-form div { - display: block; +/* Shown to editors while the plugin still needs setting up. */ +.flw-mssing-api-keys { + background: #fff6e9; + border-left: 4px solid #ff9b00; + border-radius: 8px; + color: #7a4a00; + display: block; + font-family: Moderat, Inter, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, sans-serif; + font-size: 15px; + margin: 24px auto; + max-width: 440px; + padding: 14px 16px; +} + +.flw-mssing-api-keys a { + color: inherit; + font-weight: 500; +} + +/* Redirect overlay -------------------------------------------------------- */ + +.flutterwave-overlay { + backdrop-filter: blur(2px); + background-color: rgba(16, 24, 40, 0.55); + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; + z-index: 99999; +} + +.flw-overlay-text { + background: #fff; + border-radius: 12px; + box-shadow: 0 20px 48px rgba(16, 24, 40, 0.24); + color: #101828; + font-family: Moderat, Inter, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, sans-serif; + font-size: 15px; + left: 50%; + line-height: 1.5; + max-width: calc(100% - 32px); + padding: 28px 32px; + position: absolute; + text-align: center; + top: 50%; + transform: translate(-50%, -50%); + width: 360px; +} + +.flw-overlay-spinner { + animation: flw-spin 0.9s linear infinite; + border: 3px solid #f2f4f7; + border-radius: 50%; + border-top-color: #ff9b00; + display: block; + height: 40px; + margin: 0 auto 16px; + width: 40px; +} + +@keyframes flw-spin { + + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: reduce) { + + .flw-overlay-spinner { + animation-duration: 3s; + } } -/** Flutterwave Donation Page **/ +@media screen and (max-width: 480px) { -.flutterwave-donation-form input[type="text"], -.flutterwave-donation-form input[type="number"], -.flutterwave-donation-form input[type="email"], .flutterwave-donation-form select { - background: #FFFFFF; - border: 1px solid #BDBDBD; - /*border: 1px solid #F5A623;*/ - border-radius: 4px; - padding: 15px; - font-size: 16px; - font-weight: 400; - display: flex; - justify-content: flex-start; + .flutterwave-payment-form, + .flutterwave-donation-form { + border-radius: 10px; + padding: 24px 20px; + } } diff --git a/assets/fonts/Inter-cyrillic-ext-normal.woff2 b/assets/fonts/Inter-cyrillic-ext-normal.woff2 new file mode 100644 index 0000000..405017d Binary files /dev/null and b/assets/fonts/Inter-cyrillic-ext-normal.woff2 differ diff --git a/assets/fonts/Inter-cyrillic-normal.woff2 b/assets/fonts/Inter-cyrillic-normal.woff2 new file mode 100644 index 0000000..f57ab2f Binary files /dev/null and b/assets/fonts/Inter-cyrillic-normal.woff2 differ diff --git a/assets/fonts/Inter-greek-ext-normal.woff2 b/assets/fonts/Inter-greek-ext-normal.woff2 new file mode 100644 index 0000000..2fc07e9 Binary files /dev/null and b/assets/fonts/Inter-greek-ext-normal.woff2 differ diff --git a/assets/fonts/Inter-greek-normal.woff2 b/assets/fonts/Inter-greek-normal.woff2 new file mode 100644 index 0000000..41576da Binary files /dev/null and b/assets/fonts/Inter-greek-normal.woff2 differ diff --git a/assets/fonts/Inter-latin-ext-normal.woff2 b/assets/fonts/Inter-latin-ext-normal.woff2 new file mode 100644 index 0000000..fb2522c Binary files /dev/null and b/assets/fonts/Inter-latin-ext-normal.woff2 differ diff --git a/assets/fonts/Inter-latin-normal.woff2 b/assets/fonts/Inter-latin-normal.woff2 new file mode 100644 index 0000000..937631c Binary files /dev/null and b/assets/fonts/Inter-latin-normal.woff2 differ diff --git a/assets/fonts/Inter-vietnamese-normal.woff2 b/assets/fonts/Inter-vietnamese-normal.woff2 new file mode 100644 index 0000000..e905732 Binary files /dev/null and b/assets/fonts/Inter-vietnamese-normal.woff2 differ diff --git a/assets/fonts/Millik.woff2 b/assets/fonts/Millik.woff2 new file mode 100644 index 0000000..ce3671b Binary files /dev/null and b/assets/fonts/Millik.woff2 differ diff --git a/assets/fonts/Moderat-Bold.woff2 b/assets/fonts/Moderat-Bold.woff2 new file mode 100644 index 0000000..f44b8a3 Binary files /dev/null and b/assets/fonts/Moderat-Bold.woff2 differ diff --git a/assets/fonts/Moderat-Medium.woff2 b/assets/fonts/Moderat-Medium.woff2 new file mode 100644 index 0000000..d655c1a Binary files /dev/null and b/assets/fonts/Moderat-Medium.woff2 differ diff --git a/assets/fonts/Moderat-Regular.woff2 b/assets/fonts/Moderat-Regular.woff2 new file mode 100644 index 0000000..dc9dd31 Binary files /dev/null and b/assets/fonts/Moderat-Regular.woff2 differ diff --git a/assets/images/admin/flutterwave-logo.svg b/assets/images/admin/flutterwave-logo.svg new file mode 100644 index 0000000..1249d98 --- /dev/null +++ b/assets/images/admin/flutterwave-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/admin/welcome-docs.svg b/assets/images/admin/welcome-docs.svg new file mode 100644 index 0000000..a26766b --- /dev/null +++ b/assets/images/admin/welcome-docs.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/admin/welcome-support.svg b/assets/images/admin/welcome-support.svg new file mode 100644 index 0000000..d5874de --- /dev/null +++ b/assets/images/admin/welcome-support.svg @@ -0,0 +1,373 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/bg.jpg b/assets/images/bg.jpg deleted file mode 100644 index 0859b52..0000000 Binary files a/assets/images/bg.jpg and /dev/null differ diff --git a/assets/images/bg2.jpg b/assets/images/bg2.jpg deleted file mode 100644 index c060eb6..0000000 Binary files a/assets/images/bg2.jpg and /dev/null differ diff --git a/assets/images/donation-loading.gif b/assets/images/donation-loading.gif deleted file mode 100644 index 88f6fb1..0000000 Binary files a/assets/images/donation-loading.gif and /dev/null differ diff --git a/assets/images/flutterwave-full.svg b/assets/images/flutterwave-full.svg deleted file mode 100644 index e31cd8d..0000000 --- a/assets/images/flutterwave-full.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/flutterwave-icon.png b/assets/images/flutterwave-icon.png deleted file mode 100644 index 8e13c00..0000000 Binary files a/assets/images/flutterwave-icon.png and /dev/null differ diff --git a/assets/images/flutterwave-logo-flat-inverted.svg b/assets/images/flutterwave-logo-flat-inverted.svg deleted file mode 100644 index 7ea2334..0000000 --- a/assets/images/flutterwave-logo-flat-inverted.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/flutterwave.png b/assets/images/flutterwave.png deleted file mode 100644 index b76c2d8..0000000 Binary files a/assets/images/flutterwave.png and /dev/null differ diff --git a/assets/images/flw-lock.png b/assets/images/flw-lock.png deleted file mode 100644 index d38a502..0000000 Binary files a/assets/images/flw-lock.png and /dev/null differ diff --git a/assets/images/flw-lock2.png b/assets/images/flw-lock2.png deleted file mode 100644 index 5462910..0000000 Binary files a/assets/images/flw-lock2.png and /dev/null differ diff --git a/assets/images/flw-lock4.png b/assets/images/flw-lock4.png deleted file mode 100644 index 4c5a64f..0000000 Binary files a/assets/images/flw-lock4.png and /dev/null differ diff --git a/assets/images/old-logo.svg b/assets/images/old-logo.svg deleted file mode 100644 index df9d909..0000000 --- a/assets/images/old-logo.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/images/plus.svg b/assets/images/plus.svg deleted file mode 100644 index 2f7efd4..0000000 --- a/assets/images/plus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/rave-icon.jpg b/assets/images/rave-icon.jpg deleted file mode 100644 index ea6e7c5..0000000 Binary files a/assets/images/rave-icon.jpg and /dev/null differ diff --git a/assets/js/admin/integrations.js b/assets/js/admin/integrations.js index 59ff815..d9e7673 100644 --- a/assets/js/admin/integrations.js +++ b/assets/js/admin/integrations.js @@ -1,3 +1,3 @@ -jQuery(function ($) { - $('body').addClass('.flutterwave-page'); -}); +jQuery( function ( $ ) { + $( 'body' ).addClass( '.flutterwave-page' ); +} ); diff --git a/assets/js/flw-donation.js b/assets/js/flw-donation.js index 7abdd0f..ee87a74 100644 --- a/assets/js/flw-donation.js +++ b/assets/js/flw-donation.js @@ -1,134 +1,206 @@ -var pp; +jQuery( function ( $ ) { + const options = window.flw_pay_options; -jQuery(function ($) { /** - * Builds config object to be sent to GetPaid + * Reads a data-* attribute as a raw string. * - * @return object - The config object + * @param {jQuery} form The form element. + * @param {string} name Attribute name without the data- prefix. + * @return {string|undefined} The attribute value. */ - const buildConfigObj = function (form) { - let formData = $(form).data(); - let amount = formData.amount?.replace(/"|'/g, '') || $(form).find('#flw-amount').val(); - let email = formData.email?.replace(/"|'/g, '') || $(form).find('#flw-customer-email').val(); - let firstname = - formData.firstname?.replace(/"|'/g, '') || $(form).find('#flw-first-name').val(); - let lastname = - formData.lastname?.replace(/"|'/g, '') || $(form).find('#flw-last-name').val(); - let formCurrency = - formData.currency?.replace(/"|'/g, '') || $(form).find('#flw-currency').val(); - let formId = form.attr('id'); - let txref = 'WP_' + formId.toUpperCase() + '_' + new Date().valueOf(); - let setCountry; //set country - - //switch the country with form currency provided - setCountry = flw_pay_options.countries[formCurrency] - ? flw_pay_options.countries[formCurrency] - : flw_pay_options.countries['NGN']; - - let redirect_url = window.location.origin; + const readAttr = function ( form, name ) { + const value = $( form ).attr( 'data-' + name ); + return value === undefined || value === '' ? undefined : value; + }; + + /** + * The fixed amount from data-amount, or the amount the customer typed. + * + * @param {jQuery} form The form element. + * @return {string|undefined} The amount to charge. + */ + const amountFor = function ( form ) { + const fixed = readAttr( form, 'amount' ); + + // "0" or an empty attribute means the customer chooses the amount. + if ( fixed !== undefined && parseFloat( fixed ) > 0 ) { + return fixed; + } + + return $( form ).find( '#flw-amount' ).val(); + }; + + /** + * Shows a checkout error. + * + * @param {jQuery} form The form element. + * @param {string} message The message to show. + */ + const showError = function ( form, message ) { + errorBox( form ).text( message ).addClass( 'is-visible' ); + $( form ).find( 'button' ).prop( 'disabled', false ); + }; + + /** + * The error box belonging to a form. + * + * @param {jQuery} form The form element. + * @return {jQuery} The error box. + */ + const errorBox = function ( form ) { + return $( form ) + .closest( '.flutterwave-payment-form, .flutterwave-donation-form' ) + .find( '.flw-error' ); + }; + + /** + * Builds the checkout request. + * + * @param {jQuery} form The form element. + * @return {Object} The checkout request. + */ + const buildConfigObj = function ( form ) { + const amount = amountFor( form ); + const email = + readAttr( form, 'email' ) || + $( form ).find( '#flw-customer-email' ).val(); + const firstname = + readAttr( form, 'firstname' ) || + $( form ).find( '#flw-first-name' ).val(); + const lastname = + readAttr( form, 'lastname' ) || + $( form ).find( '#flw-last-name' ).val(); + const formCurrency = + readAttr( form, 'currency' ) || + $( form ).find( '#flw-currency' ).val(); + const formId = form.attr( 'id' ); + const txref = 'WP_' + formId.toUpperCase() + '_' + new Date().valueOf(); + // Switch the country with the form currency provided. + const country = options.countries[ formCurrency ] + ? options.countries[ formCurrency ] + : options.countries.NGN; return { - amount: amount, - country: setCountry, //flw_pay_options.country, - currency: formCurrency ?? flw_pay_options.currency, + amount, + country, + currency: formCurrency ?? options.currency, customer: { email, phone_number: null, name: firstname + ' ' + lastname, }, - payment_options: flw_pay_options.method, - public_key: flw_pay_options.public_key, + payment_options: options.method, + public_key: options.public_key, tx_ref: txref, customizations: { - title: flw_pay_options.title, - description: flw_pay_options.desc, - logo: flw_pay_options.logo, + title: options.title, + description: options.desc, + logo: options.logo, }, form_id: formId, }; }; - const processCheckout = function (opts, form) { - let args = { + const processCheckout = function ( opts, form ) { + const args = { action: 'get_payment_url', - flw_sec_code: $(form).find('#flw_sec_code').val(), - payment_type: $(form).find('#flw-payment-type').val(), + flw_sec_code: $( form ).find( '#flw_sec_code' ).val(), + payment_type: $( form ).find( '#flw-payment-type' ).val(), + flw_form_config: $( form ) + .find( 'input[name="flw_form_config"]' ) + .val(), + flw_form_sig: $( form ).find( 'input[name="flw_form_sig"]' ).val(), }; - let dataObj = Object.assign({}, args, opts); - $.post(flw_pay_options.cb_url, dataObj).success(function (data) { - let response = data; + const dataObj = Object.assign( {}, args, opts ); + $.post( options.cb_url, dataObj ) + .done( function ( data ) { + const response = data; - if (response.status === 'error') { - $('.flw-error') - .html(response.message) - .attr('style', 'color:red'); - } else { - let flw_overlay = $('#flutterwave-overlay'); - flw_overlay.addClass('flutterwave-overlay'); - $('#flw-overlay-text').addClass('flw-overlay-text'); - flw_overlay.show(); - redirectTo(response.url); - } - }); + if ( response.status === 'error' ) { + showError( form, response.message ); + } else { + const overlay = $( '#flutterwave-overlay' ); + overlay.addClass( 'flutterwave-overlay' ); + $( '#flw-overlay-text' ).addClass( 'flw-overlay-text' ); + overlay.show(); + redirectTo( response.url ); + } + } ) + .fail( function ( xhr ) { + showError( + form, + ( xhr.responseJSON && xhr.responseJSON.message ) || + 'Unable to start the payment. Please try again.' + ); + } ); }; /** - * Redirect to set url - * - * @param string url - The link to redirect to + * Redirect to a url. * - * @return void + * @param {string} url The link to redirect to. */ - const redirectTo = function (url) { - if (url) { + const redirectTo = function ( url ) { + if ( url ) { location.href = url; } }; // for each form process payments - $('.flw-donation-form').each(function () { - let form = $(this); - - form.find('#flw-payment-type').on('change', function () { - let option = $(this).val(); - let btn = jQuery('.flw-donation-form').find('#flw-pay-now-button'); - let fullText = 'DONATE ' + option.toUpperCase(); - btn.text(null); - console.log(btn.text()); - btn.text(fullText); - }); - - form.on('submit', function (event) { + $( '.flw-donation-form' ).each( function () { + const form = $( this ); + + form.find( '#flw-payment-type' ).on( 'change', function () { + const option = $( this ).val(); + form.find( '#flw-pay-now-button' ).text( 'Donate ' + option ); + } ); + + // Suggested amounts fill the amount field; typing clears the selection. + const presets = form.find( '.flw-amount-preset' ); + presets.on( 'click', function () { + presets + .removeClass( 'is-selected' ) + .attr( 'aria-pressed', 'false' ); + $( this ).addClass( 'is-selected' ).attr( 'aria-pressed', 'true' ); + form.find( '#flw-amount' ).val( $( this ).data( 'amount' ) ); + } ); + form.find( '#flw-amount' ).on( 'input', function () { + presets + .removeClass( 'is-selected' ) + .attr( 'aria-pressed', 'false' ); + } ); + + form.on( 'submit', function ( event ) { event.preventDefault(); // Prevent the default form submission - let btn = form.find('button'); - btn.prop('disabled', true); + errorBox( form ).removeClass( 'is-visible' ).text( '' ); + const btn = form.find( 'button' ); + btn.prop( 'disabled', true ); - let inputs = form.find('input[type="text"]'); + const inputs = form.find( 'input[type="text"]' ); let isValid = true; - inputs.each(function () { - let inputValue = $(this).val(); + inputs.each( function () { + const inputValue = $( this ).val(); if ( typeof inputValue === 'string' && inputValue.trim() === '' ) { isValid = false; - $(this).attr('style', 'border-color: red'); + $( this ).attr( 'style', 'border-color: red' ); } else { - $(this).attr('style', 'border-color: green'); + $( this ).attr( 'style', 'border-color: green' ); } - }); + } ); - if (isValid) { - let config = buildConfigObj(form); - console.log(config); - processCheckout(config, form); + if ( isValid ) { + const config = buildConfigObj( form ); + processCheckout( config, form ); } else { //unblur button. - btn.effect('shake', { times: 2 }, 300); - btn.prop('disabled', false); + btn.effect( 'shake', { times: 2 }, 300 ); + btn.prop( 'disabled', false ); } - }); - }); -}); + } ); + } ); +} ); diff --git a/assets/js/flw-donation.min.js b/assets/js/flw-donation.min.js index 524b215..f2c4afe 100644 --- a/assets/js/flw-donation.min.js +++ b/assets/js/flw-donation.min.js @@ -1 +1 @@ -var pp;jQuery(function(f){function a(t,e){e={action:"get_payment_url",flw_sec_code:f(e).find("#flw_sec_code").val(),payment_type:f(e).find("#flw-payment-type").val()},e=Object.assign({},e,t),f.post(flw_pay_options.cb_url,e).success(function(t){if("error"===t.status)f(".flw-error").html(t.message).attr("style","color:red");else{var e=f("#flutterwave-overlay"),e=(e.addClass("flutterwave-overlay"),f("#flw-overlay-text").addClass("flw-overlay-text"),e.show(),t.url);if(e)location.href=e}})}f(".flw-donation-form").each(function(){let n=f(this);n.find("#flw-payment-type").on("change",function(){var t=f(this).val(),e=jQuery(".flw-donation-form").find("#flw-pay-now-button"),t="DONATE "+t.toUpperCase();e.text(null),console.log(e.text()),e.text(t)}),n.on("submit",function(t){t.preventDefault();var t=n.find("button"),e=(t.prop("disabled",!0),n.find('input[type="text"]'));let o=!0;e.each(function(){var t=f(this).val();"string"==typeof t&&""===t.trim()?(o=!1,f(this).attr("style","border-color: red")):f(this).attr("style","border-color: green")}),o?(e=function(t){var e=f(t).data(),o=e.amount?.replace(/"|'/g,"")||f(t).find("#flw-amount").val(),n=e.email?.replace(/"|'/g,"")||f(t).find("#flw-customer-email").val(),a=e.firstname?.replace(/"|'/g,"")||f(t).find("#flw-first-name").val(),l=e.lastname?.replace(/"|'/g,"")||f(t).find("#flw-last-name").val(),e=e.currency?.replace(/"|'/g,"")||f(t).find("#flw-currency").val(),t=t.attr("id"),r="WP_"+t.toUpperCase()+"_"+(new Date).valueOf(),i=flw_pay_options.countries[e]||flw_pay_options.countries.NGN;window.location.origin;return{amount:o,country:i,currency:e??flw_pay_options.currency,customer:{email:n,phone_number:null,name:a+" "+l},payment_options:flw_pay_options.method,public_key:flw_pay_options.public_key,tx_ref:r,customizations:{title:flw_pay_options.title,description:flw_pay_options.desc,logo:flw_pay_options.logo},form_id:t}}(n),console.log(e),a(e,n)):(t.effect("shake",{times:2},300),t.prop("disabled",!1))})})}); +jQuery(function(l){function a(t,e){r(t).text(e).addClass("is-visible"),l(t).find("button").prop("disabled",!1)}function o(t){n=t;var e=void 0!==(e=f(n,"amount"))&&0 0 ) { + return fixed; + } + + return $( form ).find( '#flw-amount' ).val(); + }; + + /** + * Shows a checkout error. + * + * @param {jQuery} form The form element. + * @param {string} message The message to show. + */ + const showError = function ( form, message ) { + errorBox( form ).text( message ).addClass( 'is-visible' ); + $( form ).find( 'button' ).prop( 'disabled', false ); + }; + /** - * Builds config object to be sent to GetPaid + * The error box belonging to a form. * - * @return object - The config object + * @param {jQuery} form The form element. + * @return {jQuery} The error box. */ - const buildConfigObj = function (form) { - let formData = $(form).data(); + const errorBox = function ( form ) { + return $( form ) + .closest( '.flutterwave-payment-form, .flutterwave-donation-form' ) + .find( '.flw-error' ); + }; + /** + * Builds the checkout request. + * + * @param {jQuery} form The form element. + * @return {Object} The checkout request. + */ + const buildConfigObj = function ( form ) { // Form Appearance settings. - let should_split_name = formData.split_name?.replace(/"|'/g, '') == 1; + const shouldSplitName = readAttr( form, 'split_name' ) === '1'; let fullname = ''; - if (!should_split_name) { - fullname = $(form).find('#flw-full-name').val(); + if ( ! shouldSplitName ) { + fullname = + readAttr( form, 'fullname' ) || + $( form ).find( '#flw-full-name' ).val(); } else { - let firstname = - formData.firstname?.replace(/"|'/g, '') || - $(form).find('#flw-first-name').val(); - let lastname = - formData.lastname?.replace(/"|'/g, '') || - $(form).find('#flw-last-name').val(); + const firstname = + readAttr( form, 'firstname' ) || + $( form ).find( '#flw-first-name' ).val(); + const lastname = + readAttr( form, 'lastname' ) || + $( form ).find( '#flw-last-name' ).val(); fullname = firstname + ' ' + lastname; } - let phone = - formData.phone?.replace(/"|'/g, '') || - $(form).find('#flw-phone').val(); - - let amount = - formData.amount?.replace(/"|'/g, '') || - $(form).find('#flw-amount').val(); - let email = - formData.email?.replace(/"|'/g, '') || - $(form).find('#flw-customer-email').val(); - let special_currency_value = - formData.custom_currency?.replace(/"|'/g, '') || - $(form).find('#flw-currency').val(); - let formCurrency = - formData.custom_currency?.replace(/"|'/g, '').length > 3 - ? $(form).find('#flw-currency').val() - : special_currency_value; - - let formId = form.attr('id'); - let txref = 'WP_' + formId.toUpperCase() + '_' + new Date().valueOf(); - let setCountry; //set country - - //switch the country with form currency provided - setCountry = flw_pay_options.countries[formCurrency] - ? flw_pay_options.countries[formCurrency] - : flw_pay_options.countries['NGN']; - - let redirect_url = window.location.origin; + const phone = + readAttr( form, 'phone' ) || $( form ).find( '#flw-phone' ).val(); + + const amount = amountFor( form ); + const email = + readAttr( form, 'email' ) || + $( form ).find( '#flw-customer-email' ).val(); + const specialCurrencyValue = + readAttr( form, 'custom_currency' ) || + $( form ).find( '#flw-currency' ).val(); + const formCurrency = + ( readAttr( form, 'custom_currency' ) || '' ).length > 3 + ? $( form ).find( '#flw-currency' ).val() + : specialCurrencyValue; + + const formId = form.attr( 'id' ); + const txref = 'WP_' + formId.toUpperCase() + '_' + new Date().valueOf(); + // Switch the country with the form currency provided. + const country = options.countries[ formCurrency ] + ? options.countries[ formCurrency ] + : options.countries.NGN; return { - amount: amount, - country: setCountry, //flw_pay_options.country, - currency: formCurrency ?? flw_pay_options.currency, + amount, + country, + currency: formCurrency ?? options.currency, customer: { email, phone_number: phone ?? null, name: fullname, }, - payment_options: flw_pay_options.method, - public_key: flw_pay_options.public_key, + payment_options: options.method, + public_key: options.public_key, tx_ref: txref, customizations: { - title: flw_pay_options.title, - description: flw_pay_options.desc, - logo: flw_pay_options.logo, + title: options.title, + description: options.desc, + logo: options.logo, }, form_id: formId, }; }; - const processCheckout = function (opts, form) { - let args = { + const processCheckout = function ( opts, form ) { + const args = { action: 'get_payment_url', - flw_sec_code: $(form).find('#flw_sec_code').val(), + flw_sec_code: $( form ).find( '#flw_sec_code' ).val(), + flw_form_config: $( form ) + .find( 'input[name="flw_form_config"]' ) + .val(), + flw_form_sig: $( form ).find( 'input[name="flw_form_sig"]' ).val(), }; - let dataObj = Object.assign({}, args, opts); - - // console.log(dataObj); + const dataObj = Object.assign( {}, args, opts ); - $.post(flw_pay_options.cb_url, dataObj).success(function (data) { - let response = data; + $.post( options.cb_url, dataObj ) + .done( function ( data ) { + const response = data; - if (response.status === 'error') { - $('.flw-error') - .html(response.message) - .attr('style', 'color:red'); - } else { - let flw_overlay = $('#flutterwave-overlay'); - flw_overlay.addClass('flutterwave-overlay'); - $('#flw-overlay-text').addClass('flw-overlay-text'); - flw_overlay.show(); - redirectTo(response.url); - } - }); - }; - - /** - * Sends payment response from GetPaid to the process payment endpoint - * - * @param object Response object from GetPaid - * - * @return void - */ - const sendPaymentRequestResponse = function (res, form) { - let args = { - action: 'process_payment', - flw_sec_code: $(form).find('#flw_sec_code').val(), - }; - - let dataObj = Object.assign({}, args, res.tx); - - $.post(flw_pay_options.cb_url, dataObj).success(function (data) { - var response = JSON.parse(data); - redirectUrl = response.redirect_url; - - if (redirectUrl === '') { - var responseMsg = - res.tx.paymentType === 'account' - ? res.tx.acctvalrespmsg - : res.tx.vbvrespmessage; - $(form) - .find('#notice') - .text(responseMsg) - .removeClass(function () { - return $(form).find('#notice').attr('class'); - }) - .addClass(response.status); - } else { - setTimeout(redirectTo, 5000, redirectUrl); - } - }); + if ( response.status === 'error' ) { + showError( form, response.message ); + } else { + const overlay = $( '#flutterwave-overlay' ); + overlay.addClass( 'flutterwave-overlay' ); + $( '#flw-overlay-text' ).addClass( 'flw-overlay-text' ); + overlay.show(); + redirectTo( response.url ); + } + } ) + .fail( function ( xhr ) { + showError( + form, + ( xhr.responseJSON && xhr.responseJSON.message ) || + 'Unable to start the payment. Please try again.' + ); + } ); }; /** - * Redirect to set url - * - * @param string url - The link to redirect to + * Redirect to a url. * - * @return void + * @param {string} url The link to redirect to. */ - const redirectTo = function (url) { - if (url) { + const redirectTo = function ( url ) { + if ( url ) { location.href = url; } }; // for each form process payments - $('.flw-simple-pay-now-form').each(function () { - let form = $(this); + $( '.flw-simple-pay-now-form' ).each( function () { + const form = $( this ); - form.on('submit', function (event) { + form.on( 'submit', function ( event ) { event.preventDefault(); // Prevent the default form submission - let btn = form.find('button'); + errorBox( form ).removeClass( 'is-visible' ).text( '' ); + const btn = form.find( 'button' ); //gray the button. - btn.prop('disabled', true); + btn.prop( 'disabled', true ); - let inputs = form.find('input[type="*"]'); + const inputs = form.find( 'input[type="*"]' ); let isValid = true; - inputs.each(function () { - let inputValue = $(this).val(); + inputs.each( function () { + const inputValue = $( this ).val(); if ( typeof inputValue === 'string' && inputValue.trim() === '' ) { isValid = false; - $(this).attr('style', 'border-color: red'); + $( this ).attr( 'style', 'border-color: red' ); } if ( - $(this).attr('type') === 'number' && - parseInt(inputValue) === NaN + $( this ).attr( 'type' ) === 'number' && + Number.isNaN( parseInt( inputValue, 10 ) ) ) { isValid = false; - $(this).attr('style', 'border-color: red'); + $( this ).attr( 'style', 'border-color: red' ); } - }); + } ); - if (isValid) { - let config = buildConfigObj(form); - console.log(config); - processCheckout(config, form); + if ( isValid ) { + const config = buildConfigObj( form ); + processCheckout( config, form ); } else { //unblur button. - btn.effect('shake', { times: 2 }, 300); - btn.prop('disabled', false); + btn.effect( 'shake', { times: 2 }, 300 ); + btn.prop( 'disabled', false ); } - }); - }); -}); + } ); + } ); +} ); diff --git a/assets/js/flw.min.js b/assets/js/flw.min.js index b9e51b3..86a5ce3 100644 --- a/assets/js/flw.min.js +++ b/assets/js/flw.min.js @@ -1 +1 @@ -jQuery(function(s){function n(e){e&&(location.href=e)}s(".flw-simple-pay-now-form").each(function(){let o=s(this);o.on("submit",function(e){e.preventDefault();var t,e=o.find("button"),l=(e.prop("disabled",!0),o.find('input[type="*"]'));let a=!0;l.each(function(){var e=s(this).val();"string"==typeof e&&""===e.trim()&&(a=!1,s(this).attr("style","border-color: red")),"number"===s(this).attr("type")&&NaN===parseInt(e)&&(a=!1,s(this).attr("style","border-color: red"))}),a?(l=function(e){var t=s(e).data(),l=1==t.split_name?.replace(/"|'/g,"");let a="";a=l?(t.firstname?.replace(/"|'/g,"")||s(e).find("#flw-first-name").val())+" "+(t.lastname?.replace(/"|'/g,"")||s(e).find("#flw-last-name").val()):s(e).find("#flw-full-name").val();var l=t.phone?.replace(/"|'/g,"")||s(e).find("#flw-phone").val(),o=t.amount?.replace(/"|'/g,"")||s(e).find("#flw-amount").val(),n=t.email?.replace(/"|'/g,"")||s(e).find("#flw-customer-email").val(),r=t.custom_currency?.replace(/"|'/g,"")||s(e).find("#flw-currency").val(),t=3{if(window.givewp&&window.givewp.gateways){let e={id:"flutterwave",Fields(){return e.settings&&e.settings.message||""}};window.givewp.gateways.register(e)}})(); diff --git a/assets/js/paymentplan.js b/assets/js/paymentplan.js index e979c36..6f308e6 100644 --- a/assets/js/paymentplan.js +++ b/assets/js/paymentplan.js @@ -1,23 +1,23 @@ -const planName = jQuery('#plan-name'); -const planAmount = jQuery('#plan-amount'); -const planInterval = jQuery('#plan-interval'); -const planDuration = jQuery('#plan-duration'); +const planName = jQuery( '#plan-name' ); +const planAmount = jQuery( '#plan-amount' ); +const planInterval = jQuery( '#plan-interval' ); +const planDuration = jQuery( '#plan-duration' ); -jQuery(document).ready(function () { - jQuery('#wpfooter').hide(); +jQuery( document ).ready( function () { + jQuery( '#wpfooter' ).hide(); - jQuery('#createPlan-btn').on('click', (event) => { + jQuery( '#createPlan-btn' ).on( 'click', ( event ) => { event.preventDefault(); jQuery - .post('#', { + .post( '#', { amount: planAmount.val(), name: planName.val(), interval: planInterval.val(), - duration: parseInt(planDuration.val()), - }) - .done(function () { + duration: parseInt( planDuration.val() ), + } ) + .done( function () { location.reload(); - }); - }); -}); + } ); + } ); +} ); diff --git a/assets/js/paymentplan.min.js b/assets/js/paymentplan.min.js index 86f0a30..d88844e 100644 --- a/assets/js/paymentplan.min.js +++ b/assets/js/paymentplan.min.js @@ -1 +1 @@ -const planName=jQuery("#plan-name"),planAmount=jQuery("#plan-amount"),planInterval=jQuery("#plan-interval"),planDuration=jQuery("#plan-duration");jQuery(document).ready(function(){jQuery("#wpfooter").hide(),jQuery("#createPlan-btn").on("click",n=>{n.preventDefault(),jQuery.post("#",{amount:planAmount.val(),name:planName.val(),interval:planInterval.val(),duration:parseInt(planDuration.val())}).done(function(){location.reload()})})}); +let planName=jQuery("#plan-name"),planAmount=jQuery("#plan-amount"),planInterval=jQuery("#plan-interval"),planDuration=jQuery("#plan-duration");jQuery(document).ready(function(){jQuery("#wpfooter").hide(),jQuery("#createPlan-btn").on("click",n=>{n.preventDefault(),jQuery.post("#",{amount:planAmount.val(),name:planName.val(),interval:planInterval.val(),duration:parseInt(planDuration.val())}).done(function(){location.reload()})})}); diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh deleted file mode 100755 index 0643775..0000000 --- a/bin/install-wp-tests.sh +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env bash - -if [ $# -lt 3 ] && [ -z $FLUTTERWAVE_PAYMENTS_DIR ]; then - echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" - exit 1 -fi - -DB_NAME=${1-flutterwave_payments_tests} -DB_USER=${2-root} -DB_PASS=${3-$MYSQL_ROOT_PASSWORD} -DB_HOST=${4-$WORDPRESS_DB_HOST} -WP_VERSION=${5-latest} -WC_VERSION=${6-latest} -SKIP_DB_CREATE=${7-false} - -TMPDIR=${TMPDIR-/tmp} -TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") -WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} -WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} - -download() { - if [ `which curl` ]; then - curl -s "$1" > "$2"; - elif [ `which wget` ]; then - wget -nv -O "$2" "$1" - fi -} - -wp() { - WORKING_DIR="$PWD" - cd "$WP_CORE_DIR" - - if [ ! -f $TMPDIR/wp-cli.phar ]; then - download https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar "$TMPDIR/wp-cli.phar" - fi - php "$TMPDIR/wp-cli.phar" $@ - - cd "$WORKING_DIR" -} - -get_db_connection_flags() { - # parse DB_HOST for port or socket references - local DB_HOST_PARTS=(${DB_HOST//\:/ }) - local DB_HOSTNAME=${DB_HOST_PARTS[0]}; - local DB_SOCK_OR_PORT=${DB_HOST_PARTS[1]}; - local EXTRA_FLAGS="" - - if ! [ -z $DB_HOSTNAME ] ; then - if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then - EXTRA_FLAGS=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" - elif ! [ -z $DB_SOCK_OR_PORT ] ; then - EXTRA_FLAGS=" --socket=$DB_SOCK_OR_PORT" - elif ! [ -z $DB_HOSTNAME ] ; then - EXTRA_FLAGS=" --host=$DB_HOSTNAME --protocol=tcp" - fi - fi - echo "--user=$DB_USER --password=$DB_PASS $EXTRA_FLAGS"; -} - - -wait_db() { - local MYSQLADMIN_FLAGS=$(get_db_connection_flags) - local WAITS=0 - - set +e - mysqladmin status $MYSQLADMIN_FLAGS > /dev/null - while [[ $? -ne 0 ]]; do - ((WAITS++)) - if [ $WAITS -ge 6 ]; then - echo "Maximum database wait time exceeded" - exit 1 - fi; - echo "Waiting until the database is available..." - sleep 5s - mysqladmin status $MYSQLADMIN_FLAGS > /dev/null - done - set -e -} - -if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then - WP_BRANCH=${WP_VERSION%\-*} - WP_TESTS_TAG="branches/$WP_BRANCH" - -elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then - WP_TESTS_TAG="branches/$WP_VERSION" -elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then - if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then - # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x - WP_TESTS_TAG="tags/${WP_VERSION%??}" - else - WP_TESTS_TAG="tags/$WP_VERSION" - fi -elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then - WP_TESTS_TAG="trunk" -else - # http serves a single offer, whereas https serves multiple. we only want one - download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json - grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json - LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') - if [[ -z "$LATEST_VERSION" ]]; then - echo "Latest WordPress version could not be found" - exit 1 - fi - WP_TESTS_TAG="tags/$LATEST_VERSION" -fi -set -ex - -install_wp() { - - if [ -d $WP_CORE_DIR ]; then - return; - fi - - mkdir -p $WP_CORE_DIR - - if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then - mkdir -p $TMPDIR/wordpress-trunk - rm -rf $TMPDIR/wordpress-trunk/* - svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress - mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR - else - if [ $WP_VERSION == 'latest' ]; then - local ARCHIVE_NAME='latest' - elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then - # https serves multiple offers, whereas http serves single. - download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json - if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then - # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x - LATEST_VERSION=${WP_VERSION%??} - else - # otherwise, scan the releases and get the most up to date minor version of the major release - local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` - LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) - fi - if [[ -z "$LATEST_VERSION" ]]; then - local ARCHIVE_NAME="wordpress-$WP_VERSION" - else - local ARCHIVE_NAME="wordpress-$LATEST_VERSION" - fi - else - local ARCHIVE_NAME="wordpress-$WP_VERSION" - fi - download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz - tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR - fi - - download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php -} - -configure_wp() { - WP_SITE_URL="http://local.wordpress.test" - wait_db - - if [[ ! -f "$WP_CORE_DIR/wp-config.php" ]]; then - wp core config --dbname=$DB_NAME --dbuser=$DB_USER --dbpass=$DB_PASS --dbhost=$DB_HOST --dbprefix=wptests_ - fi - wp core install --url="$WP_SITE_URL" --title="Example" --admin_user=admin --admin_password=password --admin_email=info@example.com --skip-email -} - -install_test_suite() { - # portable in-place argument for both GNU sed and Mac OSX sed - if [[ $(uname -s) == 'Darwin' ]]; then - local ioption='-i.bak' - else - local ioption='-i' - fi - - # set up testing suite if it doesn't yet exist - if [ ! -d $WP_TESTS_DIR ]; then - # set up testing suite - mkdir -p $WP_TESTS_DIR - rm -rf $WP_TESTS_DIR/{includes,data} - svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes - svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data - fi - - if [ ! -f wp-tests-config.php ]; then - download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php - # remove all forward slashes in the end - WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") - sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php - fi - -} - -recreate_db() { - shopt -s nocasematch - if [[ $1 =~ ^(y|yes)$ ]] - then - mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA - create_db - echo "Recreated the database ($DB_NAME)." - else - echo "Leaving the existing database ($DB_NAME) in place." - fi - shopt -u nocasematch -} - -create_db() { - mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA -} - -install_db() { - - if [ ${SKIP_DB_CREATE} = "true" ]; then - return 0 - fi - - # parse DB_HOST for port or socket references - local PARTS=(${DB_HOST//\:/ }) - local DB_HOSTNAME=${PARTS[0]}; - local DB_SOCK_OR_PORT=${PARTS[1]}; - local EXTRA="" - - if ! [ -z $DB_HOSTNAME ] ; then - if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then - EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" - elif ! [ -z $DB_SOCK_OR_PORT ] ; then - EXTRA=" --socket=$DB_SOCK_OR_PORT" - elif ! [ -z $DB_HOSTNAME ] ; then - EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" - fi - fi - - # create database - if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] - then - echo "Reinstalling will delete the existing test database ($DB_NAME)" - read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB - recreate_db $DELETE_EXISTING_DB - else - create_db - fi -} - -install_wp -install_db -configure_wp -install_test_suite \ No newline at end of file diff --git a/blocks/donation-form/block.json b/blocks/donation-form/block.json new file mode 100644 index 0000000..084ac66 --- /dev/null +++ b/blocks/donation-form/block.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "flutterwave/donation-form", + "title": "Flutterwave Donation Form", + "category": "flutterwave", + "description": "Collect one-off or recurring donations, with suggested amounts.", + "keywords": [ + "donate", + "donation", + "charity", + "give", + "recurring", + "flutterwave" + ], + "textdomain": "rave-payment-forms", + "attributes": { + "heading": { + "type": "string", + "default": "" + }, + "message": { + "type": "string", + "default": "" + }, + "currency": { + "type": "string", + "default": "" + }, + "amounts": { + "type": "string", + "default": "1000, 5000, 10000" + }, + "showFrequency": { + "type": "boolean", + "default": true + }, + "accentColor": { + "type": "string", + "default": "" + }, + "accentTextColor": { + "type": "string", + "default": "" + }, + "borderRadius": { + "type": "number" + } + }, + "supports": { + "html": false, + "align": [ + "left", + "center", + "right" + ], + "spacing": { + "margin": true + } + }, + "example": { + "attributes": { + "heading": "Support our work", + "currency": "NGN" + } + }, + "editorScript": "flw-blocks-editor", + "editorStyle": [ + "flw_css", + "flw-blocks-editor-style" + ], + "render": "file:./render.php" +} diff --git a/blocks/donation-form/render.php b/blocks/donation-form/render.php new file mode 100644 index 0000000..fbe6dd8 --- /dev/null +++ b/blocks/donation-form/render.php @@ -0,0 +1,29 @@ + sanitize_text_field( (string) ( $attributes['heading'] ?? '' ) ), + 'message' => sanitize_textarea_field( (string) ( $attributes['message'] ?? '' ) ), + 'amounts' => sanitize_text_field( (string) ( $attributes['amounts'] ?? '' ) ), + 'show_frequency' => empty( $attributes['showFrequency'] ) ? '0' : '1', +); + +if ( '' !== $currency ) { + $shortcode_atts['currency'] = $currency; +} + +printf( + '
%2$s
', + FLW_Blocks::wrapper( $attributes ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() escapes. + FLW_Shortcodes::donation_page_shortcode( $shortcode_atts, '' ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- rendered and escaped by the form view. +); diff --git a/blocks/payment-button/block.json b/blocks/payment-button/block.json new file mode 100644 index 0000000..bf1ecb8 --- /dev/null +++ b/blocks/payment-button/block.json @@ -0,0 +1,84 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "flutterwave/payment-button", + "title": "Flutterwave Payment Button", + "category": "flutterwave", + "description": "A one-line checkout for a fixed amount: an email field and a pay button.", + "keywords": [ + "pay", + "button", + "checkout", + "buy", + "flutterwave" + ], + "textdomain": "rave-payment-forms", + "attributes": { + "amount": { + "type": "number", + "default": 5000 + }, + "currency": { + "type": "string", + "default": "" + }, + "buttonText": { + "type": "string", + "default": "" + }, + "collectName": { + "type": "boolean", + "default": false + }, + "useUserEmail": { + "type": "boolean", + "default": true + }, + "width": { + "type": "string", + "enum": [ + "auto", + "full" + ], + "default": "auto" + }, + "showSecured": { + "type": "boolean", + "default": true + }, + "accentColor": { + "type": "string", + "default": "" + }, + "accentTextColor": { + "type": "string", + "default": "" + }, + "borderRadius": { + "type": "number" + } + }, + "supports": { + "html": false, + "align": [ + "left", + "center", + "right" + ], + "spacing": { + "margin": true + } + }, + "example": { + "attributes": { + "amount": 5000, + "currency": "NGN" + } + }, + "editorScript": "flw-blocks-editor", + "editorStyle": [ + "flw_css", + "flw-blocks-editor-style" + ], + "render": "file:./render.php" +} diff --git a/blocks/payment-button/render.php b/blocks/payment-button/render.php new file mode 100644 index 0000000..12b3be6 --- /dev/null +++ b/blocks/payment-button/render.php @@ -0,0 +1,41 @@ + $currency, + 'exclude' => implode( ',', $exclude ), + 'layout' => 'compact', + 'width' => 'full' === ( $attributes['width'] ?? '' ) ? 'full' : 'auto', + 'show_secured' => empty( $attributes['showSecured'] ) ? '0' : '1', + 'use_current_user_email' => empty( $attributes['useUserEmail'] ) ? 'no' : 'yes', +); + +if ( $amount > 0 ) { + $shortcode_atts['amount'] = (string) $amount; +} + +$label = trim( (string) ( $attributes['buttonText'] ?? '' ) ); +$label = '' !== $label ? $label : __( 'Pay {amount}', 'rave-payment-forms' ); + +printf( + '
%2$s
', + FLW_Blocks::wrapper( $attributes ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() escapes. + FLW_Shortcodes::pay_button_shortcode( $shortcode_atts, $label ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- rendered and escaped by the form view. +); diff --git a/blocks/payment-form/block.json b/blocks/payment-form/block.json new file mode 100644 index 0000000..cb03391 --- /dev/null +++ b/blocks/payment-form/block.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "flutterwave/payment-form", + "title": "Flutterwave Payment Form", + "category": "flutterwave", + "description": "A full payment form with a fixed or customer-entered amount.", + "keywords": [ + "pay", + "form", + "checkout", + "invoice", + "flutterwave" + ], + "textdomain": "rave-payment-forms", + "attributes": { + "heading": { + "type": "string", + "default": "" + }, + "description": { + "type": "string", + "default": "" + }, + "amount": { + "type": "number", + "default": 0 + }, + "currency": { + "type": "string", + "default": "" + }, + "collectName": { + "type": "string", + "enum": [ + "full", + "split", + "none" + ], + "default": "full" + }, + "collectPhone": { + "type": "boolean", + "default": true + }, + "buttonText": { + "type": "string", + "default": "" + }, + "showSecured": { + "type": "boolean", + "default": true + }, + "accentColor": { + "type": "string", + "default": "" + }, + "accentTextColor": { + "type": "string", + "default": "" + }, + "borderRadius": { + "type": "number" + } + }, + "supports": { + "html": false, + "align": [ + "left", + "center", + "right" + ], + "spacing": { + "margin": true + } + }, + "example": { + "attributes": { + "heading": "Pay an invoice", + "amount": 0 + } + }, + "editorScript": "flw-blocks-editor", + "editorStyle": [ + "flw_css", + "flw-blocks-editor-style" + ], + "render": "file:./render.php" +} diff --git a/blocks/payment-form/render.php b/blocks/payment-form/render.php new file mode 100644 index 0000000..ef65629 --- /dev/null +++ b/blocks/payment-form/render.php @@ -0,0 +1,48 @@ + sanitize_text_field( (string) ( $attributes['heading'] ?? '' ) ), + 'description' => sanitize_textarea_field( (string) ( $attributes['description'] ?? '' ) ), + 'split_name' => 'split' === $name_fields ? '1' : '0', + 'show_secured' => empty( $attributes['showSecured'] ) ? '0' : '1', +); + +if ( $exclude ) { + $shortcode_atts['exclude'] = implode( ',', $exclude ); +} + +if ( $amount > 0 ) { + $shortcode_atts['amount'] = (string) $amount; +} + +if ( '' !== $currency ) { + $shortcode_atts['currency'] = $currency; +} + +printf( + '
%2$s
', + FLW_Blocks::wrapper( $attributes ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() escapes. + FLW_Shortcodes::pay_button_shortcode( $shortcode_atts, trim( (string) ( $attributes['buttonText'] ?? '' ) ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- rendered and escaped by the form view. +); diff --git a/blocks/payment-methods/block.json b/blocks/payment-methods/block.json new file mode 100644 index 0000000..9805ac0 --- /dev/null +++ b/blocks/payment-methods/block.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "flutterwave/payment-methods", + "title": "Flutterwave Payment Methods", + "category": "flutterwave", + "description": "Shows the payment methods your forms accept, taken from your Flutterwave settings.", + "keywords": [ + "accepted", + "cards", + "badge", + "trust", + "secure", + "flutterwave" + ], + "textdomain": "rave-payment-forms", + "attributes": { + "label": { + "type": "string", + "default": "We accept" + }, + "showSecured": { + "type": "boolean", + "default": true + } + }, + "supports": { + "html": false, + "align": [ + "left", + "center", + "right" + ], + "spacing": { + "margin": true, + "padding": true + } + }, + "example": { + "attributes": {} + }, + "editorScript": "flw-blocks-editor", + "editorStyle": [ + "flw_css", + "flw-blocks-editor-style" + ], + "render": "file:./render.php" +} diff --git a/blocks/payment-methods/render.php b/blocks/payment-methods/render.php new file mode 100644 index 0000000..a627501 --- /dev/null +++ b/blocks/payment-methods/render.php @@ -0,0 +1,68 @@ + '', + 'bank' => '', + 'phone' => '', + 'wallet' => '', + 'coin' => '', + 'code' => '', +); + +$methods = array( + 'card' => array( __( 'Cards', 'rave-payment-forms' ), 'card' ), + 'banktransfer' => array( __( 'Bank transfer', 'rave-payment-forms' ), 'bank' ), + 'mobilemoney' => array( __( 'Mobile money', 'rave-payment-forms' ), 'phone' ), + 'applepay' => array( __( 'Apple Pay', 'rave-payment-forms' ), 'wallet' ), + 'googlepay' => array( __( 'Google Pay', 'rave-payment-forms' ), 'wallet' ), + 'opay' => array( __( 'OPay', 'rave-payment-forms' ), 'wallet' ), + 'stablecoin' => array( __( 'Stablecoin', 'rave-payment-forms' ), 'coin' ), + 'ussd' => array( __( 'USSD', 'rave-payment-forms' ), 'phone' ), + 'qr' => array( __( 'QR', 'rave-payment-forms' ), 'code' ), + 'nqr' => array( __( 'NQR', 'rave-payment-forms' ), 'code' ), + 'credit' => array( __( 'Credit', 'rave-payment-forms' ), 'card' ), + 'barter' => array( __( 'Barter', 'rave-payment-forms' ), 'wallet' ), +); + +$options = FLW_Settings::payment_options(); +$enabled = array_merge( FLW_Settings::methods_from_option( $options ), FLW_Settings::advanced_from_option( $options ) ); +$label = trim( (string) ( $attributes['label'] ?? '' ) ); +?> +
> +
+ +

+ +
    + + +
  • + +
+ +

+ + Flutterwave' + ); + ?> +

+ +
+
diff --git a/blocks/pricing-card/block.json b/blocks/pricing-card/block.json new file mode 100644 index 0000000..58e92e8 --- /dev/null +++ b/blocks/pricing-card/block.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "flutterwave/pricing-card", + "title": "Flutterwave Pricing Card", + "category": "flutterwave", + "description": "A plan or product card with a price, features and checkout.", + "keywords": [ + "price", + "plan", + "product", + "pricing", + "tier", + "flutterwave" + ], + "textdomain": "rave-payment-forms", + "attributes": { + "planName": { + "type": "string", + "default": "Starter" + }, + "amount": { + "type": "number", + "default": 25000 + }, + "currency": { + "type": "string", + "default": "" + }, + "period": { + "type": "string", + "default": "" + }, + "description": { + "type": "string", + "default": "" + }, + "features": { + "type": "array", + "default": [], + "items": { + "type": "string" + } + }, + "badge": { + "type": "string", + "default": "" + }, + "highlighted": { + "type": "boolean", + "default": false + }, + "buttonText": { + "type": "string", + "default": "" + }, + "accentColor": { + "type": "string", + "default": "" + }, + "accentTextColor": { + "type": "string", + "default": "" + }, + "borderRadius": { + "type": "number" + } + }, + "supports": { + "html": false, + "align": [ + "left", + "center", + "right" + ], + "spacing": { + "margin": true + } + }, + "example": { + "attributes": { + "planName": "Pro", + "amount": 25000, + "currency": "NGN", + "period": "one-off", + "features": [ + "Unlimited projects", + "Priority support" + ], + "badge": "Popular", + "highlighted": true + } + }, + "editorScript": "flw-blocks-editor", + "editorStyle": [ + "flw_css", + "flw-blocks-editor-style" + ], + "render": "file:./render.php" +} diff --git a/blocks/pricing-card/render.php b/blocks/pricing-card/render.php new file mode 100644 index 0000000..a83af34 --- /dev/null +++ b/blocks/pricing-card/render.php @@ -0,0 +1,64 @@ + $currency, + 'exclude' => 'fullname,phone', + 'layout' => 'compact', + 'width' => 'full', + 'show_secured' => '1', + 'use_current_user_email' => 'yes', +); + +if ( $amount > 0 ) { + $checkout['amount'] = (string) $amount; +} + +$classes = 'flw-pricing-card' . ( empty( $attributes['highlighted'] ) ? '' : ' is-highlighted' ); +$check = ''; +?> +
> +
+ + + +

+

+ + + + +

+ +

+ + +
    + +
  • + +
+ + +
+
diff --git a/client/admin/admin.scss b/client/admin/admin.scss new file mode 100644 index 0000000..dfdef93 --- /dev/null +++ b/client/admin/admin.scss @@ -0,0 +1,2297 @@ +/** + * Flutterwave admin styles. + * + * Scoped under .flw-admin-root so nothing leaks into the rest of wp-admin, and + * the WordPress page wrapper padding is neutralised so the screen sits flush + * against the admin chrome the way the designs show it. The webfonts come from + * assets/css/flw-fonts.css, which the payment forms share. + */ + +$flw-orange: #ff9b00; +$flw-orange-soft: #fdc46b; +$flw-orange-ink: #7a4a00; +$flw-ink: #101828; +$flw-body: #475467; +$flw-muted: #98a2b3; +$flw-border: #d0d5dd; +$flw-line: #eaecf0; +$flw-surface: #fff; +$flw-radius: 8px; +$flw-container: 1560px; +$flw-gutter: 48px; +$flw-serif: "Millik", "Iowan Old Style", "Palatino Linotype", palatino, georgia, serif; +$flw-sans: "Moderat", "Inter", -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif; + +/* Neutralise the wp-admin content padding for our full-bleed page. */ +#wpcontent:has(.flw-admin-root) { + padding-left: 0; +} + +.flw-admin-root { + background: $flw-surface; + color: $flw-ink; + font-family: $flw-sans; + font-size: 14px; + line-height: 1.5; + min-height: calc(100vh - 32px); + + *, + *::before, + *::after { + box-sizing: border-box; + } + + a { + color: $flw-orange; + font-weight: 500; + text-decoration: underline; + + &:hover, + &:focus { + color: #d68300; + } + } + + h1, + h2 { + font-family: $flw-serif; + font-weight: 400; + margin: 0; + } +} + +/* -------------------------------------------------------------------------- */ + +/* Page shell */ + +/* -------------------------------------------------------------------------- */ + +/* The screen is centred in a container rather than hugging the admin sidebar, + and the two columns follow the ~1.4:1 form-to-stepper ratio from the designs + instead of two fixed widths. */ +.flw-page { + + &__crumb { + border-bottom: 1px solid $flw-line; + } + + &__crumb-inner { + align-items: center; + display: flex; + margin: 0 auto; + max-width: $flw-container; + min-height: 56px; + padding: 0 $flw-gutter; + width: 100%; + } + + &__body { + display: block; + margin: 0 auto; + max-width: $flw-container; + padding: 40px $flw-gutter 80px; + + /* The designs put the form, the gutter between the columns and the + stepper in a 774:184:500 ratio. Encoding that directly — including + the gap as a percentage of the same track — keeps the proportions + identical at any width instead of approximating them. */ + &.has-aside { + column-gap: 12.6%; + display: grid; + grid-template-columns: minmax(0, 774fr) minmax(0, 500fr); + row-gap: 40px; + } + } + + &__main { + min-width: 0; + } +} + +.flw-crumb-btn { + align-items: center; + background: none; + border: 0; + color: $flw-ink; + cursor: pointer; + display: inline-flex; + font-size: 14px; + font-weight: 500; + gap: 10px; + padding: 8px 0; + + &--static { + cursor: default; + } + + &:hover:not(&--static) { + color: $flw-orange; + } +} + +/* -------------------------------------------------------------------------- */ + +/* Typography */ + +/* -------------------------------------------------------------------------- */ + +.flw-heading { + font-size: 28px; + letter-spacing: -0.01em; + margin: 16px 0 4px; +} + +.flw-subheading { + color: $flw-body; + margin: 0; +} + +.flw-rule { + background: $flw-orange-soft; + border: 0; + height: 2px; + margin: 18px 0 28px; +} + +.flw-step-badge { + background: #fcd9a0; + border-radius: 4px; + color: #4b2e00; + display: inline-block; + font-size: 12px; + padding: 5px 10px; +} + +/* -------------------------------------------------------------------------- */ + +/* Stepper */ + +/* -------------------------------------------------------------------------- */ + +.flw-stepper { + border: 1px solid $flw-line; + border-radius: 10px; + padding: 24px 28px; + + &__item { + align-items: center; + color: $flw-muted; + display: flex; + gap: 14px; + position: relative; + + &:not(:last-child) { + padding-bottom: 26px; + + &::before { + background: $flw-line; + bottom: 4px; + content: ""; + left: 11px; + position: absolute; + top: 24px; + width: 2px; + } + } + + &.is-done { + color: $flw-ink; + + .flw-stepper__dot { + background: #d1fadf; + border-color: #12b76a; + color: #039855; + } + + &::before { + background: #12b76a; + } + } + + &.is-active { + color: $flw-ink; + + .flw-stepper__dot { + background: #e4e7ec; + border-color: #98a2b3; + color: $flw-ink; + } + } + } + + &__dot { + align-items: center; + background: $flw-surface; + border: 2px solid $flw-border; + border-radius: 50%; + display: inline-flex; + flex: 0 0 auto; + height: 24px; + justify-content: center; + width: 24px; + } + + &__label { + font-size: 15px; + } +} + +/* -------------------------------------------------------------------------- */ + +/* Fields */ + +/* -------------------------------------------------------------------------- */ + +.flw-field { + margin-bottom: 22px; + + &__label { + align-items: center; + color: $flw-ink; + display: flex; + font-size: 14px; + gap: 6px; + margin-bottom: 6px; + } + + &__hint, + &__error { + align-items: flex-start; + display: flex; + font-size: 13px; + gap: 6px; + margin: 8px 0 0; + } + + &__hint { + color: $flw-muted; + + svg { + flex: 0 0 auto; + margin-top: 2px; + } + } + + &__error { + color: #d92d20; + } + + &.has-error .flw-input { + border-color: #f04438; + } +} + +.flw-input { + background: $flw-surface; + border: 1px solid $flw-border; + border-radius: $flw-radius; + color: $flw-ink; + display: block; + font-family: inherit; + font-size: 14px; + line-height: 1.4; + padding: 12px 14px; + width: 100%; + + &::placeholder { + color: $flw-muted; + } + + /* wp-admin paints its own blue focus ring via `input[type="text"]:focus`, + which outranks a lone class. Restated at higher specificity below. */ + + &[readonly] { + background: #f9fafb; + color: $flw-body; + } + + &--area { + min-height: 140px; + resize: vertical; + } +} + +/* wp-admin's forms.css caps every select at 25rem (400px) and styles focus on + element selectors, both of which outrank the plain `.flw-input` class. These + need the extra specificity to land. */ +.flw-admin-root .flw-input { + background-color: $flw-surface; + border: 1px solid $flw-border; + border-radius: $flw-radius; + box-shadow: none; + color: $flw-ink; + font-size: 14px; + line-height: 1.4; + max-width: none; + min-height: 46px; + padding: 12px 14px; + + &:focus { + border-color: $flw-orange; + box-shadow: 0 0 0 3px rgba(255, 155, 0, 0.18); + outline: none; + } +} + +/* wp-admin gives selects its own padding and arrow image; + the chevron here is ours. */ +.flw-admin-root select.flw-input { + background-image: none; + height: auto; + line-height: 1.4; + padding-right: 42px; +} + +.flw-admin-root .flw-input[readonly] { + background-color: #f9fafb; + color: $flw-body; +} + +.flw-select { + position: relative; + + select { + appearance: none; + cursor: pointer; + padding-right: 42px; + } + + &__chevron { + align-items: center; + color: $flw-body; + display: flex; + height: 100%; + pointer-events: none; + position: absolute; + right: 14px; + top: 0; + } +} + +.flw-copyfield { + display: flex; + + .flw-input { + border-bottom-right-radius: 0; + border-right: 0; + border-top-right-radius: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__btn { + background: #808080; + border: 0; + border-radius: 0 $flw-radius $flw-radius 0; + color: #fff; + cursor: pointer; + flex: 0 0 auto; + font-size: 14px; + min-width: 84px; + padding: 0 18px; + + &:hover { + background: #6e6e6e; + } + } +} + +/* Two short fields side by side, e.g. currency and country. */ +.flw-field-row { + column-gap: 20px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.flw-section-title { + border-top: 1px solid $flw-line; + font-size: 20px; + margin: 34px 0 4px; + padding-top: 26px; +} + +.flw-section-note { + color: $flw-body; + margin: 0 0 18px; +} + +/* Shows whether the entered keys will take real money. */ +.flw-mode { + border-radius: $flw-radius; + font-size: 13px; + margin: 0 0 22px; + padding: 10px 14px; + + &--test { + background: #fff6e9; + color: $flw-orange-ink; + } + + &--live { + background: #ecfdf3; + color: #05603a; + } +} + +/* -------------------------------------------------------------------------- */ + +/* Checkboxes */ + +/* -------------------------------------------------------------------------- */ + +.flw-checkbox { + align-items: center; + cursor: pointer; + display: flex; + gap: 10px; + margin: 6px 0; + + input { + height: 0; + opacity: 0; + position: absolute; + width: 0; + } + + &__box { + align-items: center; + background: $flw-surface; + border: 1.5px solid $flw-border; + border-radius: 4px; + color: transparent; + display: inline-flex; + flex: 0 0 auto; + height: 18px; + justify-content: center; + width: 18px; + } + + input:checked + &__box { + background: $flw-surface; + border-color: $flw-orange; + color: $flw-orange; + } + + input:focus-visible + &__box { + box-shadow: 0 0 0 3px rgba(255, 155, 0, 0.25); + } + + &__label { + font-size: 14px; + } +} + +/* -------------------------------------------------------------------------- */ + +/* Buttons */ + +/* -------------------------------------------------------------------------- */ + +.flw-actions { + display: flex; + gap: 16px; + margin-top: 34px; + + .flw-btn { + flex: 1 1 0; + } + + &:not(.has-back) .flw-btn { + width: 100%; + } +} + +.flw-btn { + align-items: center; + border: 0; + border-radius: $flw-radius; + cursor: pointer; + display: inline-flex; + font-family: inherit; + font-size: 15px; + font-weight: 500; + gap: 10px; + justify-content: center; + min-height: 48px; + padding: 0 28px; + transition: background-color 0.15s ease; + + &--primary { + background: $flw-orange; + color: #1a1a1a; + + &:hover:not(:disabled) { + background: #e08c00; + } + + &:disabled { + background: $flw-orange-soft; + color: rgba(26, 26, 26, 0.7); + cursor: not-allowed; + } + } + + &--secondary { + background: #808080; + color: #fff; + + &:hover:not(:disabled) { + background: #6e6e6e; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + } + + &__spinner { + animation: flw-spin 0.7s linear infinite; + border: 2px solid currentcolor; + border-radius: 50%; + border-right-color: transparent; + height: 16px; + width: 16px; + } +} + +.flw-link-btn { + background: none; + border: 0; + color: $flw-orange; + cursor: pointer; + font-family: inherit; + font-size: 14px; + font-weight: 500; + margin-top: 18px; + text-decoration: underline; +} + +/* -------------------------------------------------------------------------- */ + +/* Tooltip */ + +/* -------------------------------------------------------------------------- */ + +.flw-tooltip { + color: $flw-muted; + cursor: help; + display: inline-flex; + position: relative; + + &__bubble { + background: #2f2f2f; + border-radius: 8px; + bottom: calc(100% + 10px); + color: #fff; + font-size: 13px; + left: 0; + line-height: 1.45; + opacity: 0; + padding: 12px 14px; + pointer-events: none; + position: absolute; + transition: opacity 0.12s ease; + width: 240px; + z-index: 20; + + &::after { + border: 6px solid transparent; + border-top-color: #2f2f2f; + content: ""; + left: 12px; + position: absolute; + top: 100%; + } + } + + &:hover &__bubble, + &:focus &__bubble, + &:focus-visible &__bubble { + opacity: 1; + } +} + +/* -------------------------------------------------------------------------- */ + +/* Welcome */ + +/* -------------------------------------------------------------------------- */ + +.flw-welcome { + margin: 0 auto; + max-width: 1120px; + padding: 56px 0 80px; + text-align: center; + + .flw-wordmark { + display: block; + height: 40px; + margin: 0 auto; + width: auto; + } + + &__title { + font-size: 46px; + line-height: 1.12; + margin: 28px 0 20px; + } + + &__lede { + color: $flw-body; + font-size: 16px; + margin: 0 auto 34px; + max-width: 740px; + } +} + +.flw-cards { + display: grid; + gap: 32px; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + margin-top: 76px; + text-align: left; +} + +.flw-card { + + h2 { + font-size: 20px; + margin: 20px 0 8px; + } + + p { + color: $flw-body; + margin: 0 0 12px; + } + + /* The exported artwork carries its own rounded tinted background, so the + wrapper only has to clip it. */ + &__art { + border-radius: 10px; + display: flex; + overflow: hidden; + } +} + +.flw-illustration { + display: block; + height: auto; + width: 100%; +} + +/* -------------------------------------------------------------------------- */ + +/* Payment methods */ + +/* -------------------------------------------------------------------------- */ + +.flw-methods { + border: 1px solid $flw-line; + border-radius: 10px; + overflow: hidden; +} + +.flw-method { + align-items: flex-start; + cursor: pointer; + display: grid; + gap: 16px; + grid-template-columns: 18px 56px minmax(0, 1fr); + padding: 20px 22px; + + &:not(:last-child) { + border-bottom: 1px solid $flw-line; + } + + &:hover { + background: #fcfcfd; + } + + input { + height: 0; + opacity: 0; + position: absolute; + width: 0; + } + + .flw-checkbox__box { + margin-top: 2px; + } + + input:checked + .flw-checkbox__box { + border-color: $flw-orange; + color: $flw-orange; + } + + input:focus-visible + .flw-checkbox__box { + box-shadow: 0 0 0 3px rgba(255, 155, 0, 0.25); + } + + &__mark { + align-items: center; + border: 1px solid $flw-line; + border-radius: 6px; + display: flex; + height: 36px; + justify-content: center; + width: 56px; + } + + &__text { + display: block; + min-width: 0; + } + + &__title { + display: block; + font-size: 15px; + font-weight: 500; + } + + &__desc { + color: $flw-body; + display: block; + font-size: 13px; + margin-top: 2px; + } +} + +.flw-method-mark { + height: 24px; + width: 40px; +} + +/* -------------------------------------------------------------------------- */ + +/* Advanced disclosure */ + +/* -------------------------------------------------------------------------- */ + +.flw-advanced { + border-top: 1px solid $flw-line; + margin-top: 26px; + padding-top: 18px; + + &__toggle { + align-items: center; + background: none; + border: 0; + color: $flw-body; + cursor: pointer; + display: inline-flex; + font-family: inherit; + font-size: 14px; + font-weight: 500; + gap: 8px; + padding: 4px 0; + + &:hover { + color: $flw-ink; + } + } + + &__caret { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid currentcolor; + transition: transform 0.15s ease; + + &.is-open { + transform: rotate(180deg); + } + } + + &__body { + padding-top: 16px; + } + + &__note { + color: $flw-muted; + font-size: 13px; + margin: 0 0 14px; + } + + &__grid { + display: grid; + gap: 4px 24px; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + margin-bottom: 22px; + } +} + +/* -------------------------------------------------------------------------- */ + +/* Tabs */ + +/* -------------------------------------------------------------------------- */ + +.flw-tabs { + border-bottom: 1px solid $flw-line; + display: flex; + gap: 8px; + margin-bottom: 30px; +} + +.flw-tab { + background: none; + border: 0; + border-bottom: 2px solid transparent; + color: $flw-body; + cursor: pointer; + font-family: inherit; + font-size: 14px; + margin-bottom: -1px; + padding: 10px 14px; + + &:hover { + color: $flw-ink; + } + + &.is-active { + border-bottom-color: $flw-orange; + color: $flw-orange; + font-weight: 500; + } +} + +.flw-tabpanel { + max-width: 720px; +} + +/* -------------------------------------------------------------------------- */ + +/* Loading & success */ + +/* -------------------------------------------------------------------------- */ + +.flw-loading { + align-items: center; + display: flex; + justify-content: center; + min-height: 60vh; + + &__ring { + animation: flw-spin 0.9s linear infinite; + border: 3px solid #f2f4f7; + border-radius: 50%; + border-top-color: $flw-orange; + display: block; + height: 52px; + width: 52px; + } +} + +.flw-success { + margin: 0 auto; + max-width: 480px; + padding: 18vh 20px 80px; + text-align: center; + + h1 { + color: $flw-orange; + font-size: 24px; + margin: 24px 0 10px; + } + + p { + color: $flw-body; + margin: 0; + } +} + +.flw-success-art { + height: 96px; + width: 96px; +} + +.flw-success__shortcode { + margin: 28px auto 0; + max-width: 420px; + text-align: left; + + .flw-field__hint { + display: block; + } +} + +.flw-success .flw-actions { + margin-left: auto; + margin-right: auto; + max-width: 420px; +} + +/* -------------------------------------------------------------------------- */ + +/* Toast */ + +/* -------------------------------------------------------------------------- */ + +.flw-toast { + align-items: center; + background: #fff; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(16, 24, 40, 0.16); + display: flex; + font-size: 14px; + gap: 10px; + padding: 12px 14px; + position: fixed; + right: 24px; + top: 56px; + z-index: 9999; + + &--success { + border-left: 4px solid #12b76a; + } + + &--error { + border-left: 4px solid #f04438; + } + + &__icon { + align-items: center; + background: #12b76a; + border-radius: 50%; + color: #fff; + display: inline-flex; + height: 20px; + justify-content: center; + width: 20px; + } + + &__close { + background: none; + border: 0; + color: $flw-muted; + cursor: pointer; + font-size: 20px; + line-height: 1; + padding: 0 2px; + } +} + +@keyframes flw-spin { + + to { + transform: rotate(360deg); + } +} + +/* -------------------------------------------------------------------------- */ + +/* List screens: Payment Forms and Transactions */ + +/* -------------------------------------------------------------------------- */ + +$flw-grey: #f2f4f7; +$flw-shadow: 0 12px 32px rgba(16, 24, 40, 0.12), 0 2px 6px rgba(16, 24, 40, 0.06); + +.flw-btn--outline { + background: $flw-surface; + border: 1px solid $flw-border; + color: $flw-ink; + min-height: 40px; + padding: 0 18px; + + &:hover:not(:disabled) { + background: $flw-grey; + } +} + +.flw-list-header { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 16px; + justify-content: space-between; + margin-bottom: 20px; + + .flw-btn { + min-height: 40px; + padding: 0 18px; + } +} + +.flw-admin-root .flw-list-title { + font-size: 28px; + line-height: 1.2; +} + +.flw-list-actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.flw-admin-root .flw-toolbar-btn { + align-items: center; + background: $flw-grey; + border: 0; + border-radius: $flw-radius; + color: $flw-ink; + cursor: pointer; + display: inline-flex; + font-family: inherit; + font-size: 14px; + font-weight: 500; + gap: 8px; + min-height: 40px; + padding: 0 16px; + text-decoration: none; + + &:hover, + &[aria-expanded="true"] { + background: #e4e7ec; + color: $flw-ink; + } + + svg { + height: 16px; + width: 16px; + } +} + +.flw-tab__count { + background: $flw-grey; + border-radius: 999px; + color: $flw-body; + font-size: 12px; + margin-left: 6px; + padding: 1px 8px; + + .is-active & { + background: #fff3e0; + color: $flw-orange-ink; + } +} + +/* Popovers */ + +.flw-popover-anchor { + position: relative; +} + +.flw-popover { + background: $flw-surface; + border: 1px solid $flw-line; + border-radius: 12px; + box-shadow: $flw-shadow; + max-width: calc(100vw - 32px); + padding: 20px; + position: absolute; + right: 0; + top: calc(100% + 8px); + width: 380px; + z-index: 20; + + &__section { + border-top: 1px solid $flw-line; + margin-top: 16px; + padding-top: 16px; + + h3 { + font-size: 13px; + font-weight: 500; + margin: 0 0 10px; + } + } + + &__note { + align-items: center; + color: $flw-body; + display: flex; + font-size: 12px; + gap: 6px; + margin: 10px 0 0; + + svg { + flex: none; + height: 14px; + width: 14px; + } + } + + &__footer { + display: flex; + gap: 10px; + justify-content: flex-end; + margin-top: 20px; + + .flw-btn { + min-height: 40px; + padding: 0 18px; + } + } +} + +.flw-presets { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.flw-preset { + background: $flw-surface; + border: 1px solid $flw-border; + border-radius: 999px; + color: $flw-body; + cursor: pointer; + font-family: inherit; + font-size: 13px; + padding: 6px 14px; + + &:hover { + border-color: $flw-muted; + } + + &.is-active { + background: #fff3e0; + border-color: $flw-orange; + color: $flw-orange-ink; + font-weight: 500; + } +} + +.flw-daterange { + display: grid; + gap: 10px; + grid-template-columns: 1fr 1fr; +} + +.flw-admin-root .flw-daterange__input, +.flw-admin-root .flw-multiselect__control { + background: $flw-surface; + border: 1px solid $flw-border; + border-radius: $flw-radius; + color: $flw-ink; + font-family: inherit; + font-size: 14px; + min-height: 40px; + padding: 0 12px; + width: 100%; + + &:focus { + border-color: $flw-orange; + box-shadow: 0 0 0 3px rgba(255, 155, 0, 0.18); + outline: none; + } +} + +.flw-filter-field { + display: block; + + &__label { + display: block; + font-size: 13px; + font-weight: 500; + margin-bottom: 8px; + } +} + +.flw-multiselect { + position: relative; + + .flw-multiselect__control { + cursor: pointer; + overflow: hidden; + padding-right: 36px; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + } + + .is-placeholder, + input::placeholder { + color: $flw-muted; + } + + &__chevron { + color: $flw-muted; + pointer-events: none; + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + + .is-open & { + transform: translateY(-50%) rotate(180deg); + } + } + + &__list { + background: $flw-surface; + border: 1px solid $flw-line; + border-radius: $flw-radius; + box-shadow: $flw-shadow; + left: 0; + list-style: none; + margin: 4px 0 0; + max-height: 220px; + overflow-y: auto; + padding: 4px; + position: absolute; + right: 0; + z-index: 2; + + li { + margin: 0; + } + } + + &__option { + align-items: center; + border-radius: 6px; + cursor: pointer; + display: flex; + gap: 10px; + padding: 8px 10px; + + &:hover { + background: $flw-grey; + } + + input { + height: 1px; + opacity: 0; + position: absolute; + width: 1px; + } + } + + &__box { + align-items: center; + border: 1px solid $flw-border; + border-radius: 4px; + color: transparent; + display: inline-flex; + flex: none; + height: 16px; + justify-content: center; + width: 16px; + + svg { + height: 10px; + width: 10px; + } + + input:checked + & { + background: $flw-orange; + border-color: $flw-orange; + color: #fff; + } + + input:focus-visible + & { + box-shadow: 0 0 0 3px rgba(255, 155, 0, 0.3); + } + } + + &__empty { + color: $flw-muted; + padding: 8px 10px; + } +} + +/* Tables */ + +.flw-table-wrap { + border: 1px solid $flw-line; + border-radius: 12px; + overflow-x: auto; + + &.is-loading { + opacity: 0.6; + pointer-events: none; + } +} + +.flw-skeleton { + animation: flw-pulse 1.2s ease-in-out infinite; + background: linear-gradient(to bottom, $flw-grey 0 44px, transparent 44px) 0 0 / 100% 60px repeat-y; + height: 300px; +} + +@keyframes flw-pulse { + + 50% { + opacity: 0.5; + } +} + +.flw-table { + border-collapse: collapse; + min-width: 640px; + width: 100%; + + th { + background: $flw-grey; + color: $flw-body; + font-size: 13px; + font-weight: 500; + padding: 12px 20px; + text-align: left; + white-space: nowrap; + } + + td { + border-top: 1px solid $flw-line; + padding: 16px 20px; + vertical-align: middle; + } + + tbody tr.is-clickable { + cursor: pointer; + + &:hover, + &:focus-visible { + background: #fcfcfd; + outline: none; + } + } + + &__actions { + text-align: right; + white-space: nowrap; + } +} + +.flw-cell-primary { + color: $flw-ink; + display: block; + font-weight: 500; +} + +.flw-cell-secondary { + color: $flw-muted; + display: block; + font-size: 13px; +} + +.flw-cell-muted { + color: $flw-muted; +} + +.flw-admin-root .flw-row-link { + align-items: center; + color: $flw-body; + display: inline-flex; + gap: 4px; + margin-left: 16px; + text-decoration: none; + + &:hover { + color: $flw-orange; + } + + svg { + height: 12px; + width: 12px; + } +} + +.flw-form-cell { + align-items: center; + display: flex; + gap: 12px; + + &__icon { + align-items: center; + background: #fff3e0; + border-radius: $flw-radius; + color: $flw-orange-ink; + display: inline-flex; + flex: none; + height: 36px; + justify-content: center; + width: 36px; + + svg { + fill: currentcolor; + height: 20px; + width: 20px; + } + } +} + +.flw-pill { + background: $flw-grey; + border-radius: 999px; + color: $flw-body; + display: inline-block; + font-size: 12px; + font-weight: 500; + padding: 3px 10px; + white-space: nowrap; + + &--successful, + &--publish { + background: #ecfdf3; + color: #027a48; + } + + &--pending, + &--future { + background: #fffaeb; + color: #b54708; + } + + &--failed { + background: #fef3f2; + color: #b42318; + } + + &--review { + background: #f4f3ff; + color: #5925dc; + } + + &--synced { + background: #eff8ff; + color: #175cd3; + } +} + +.flw-pagination { + align-items: center; + display: flex; + gap: 10px; + margin-top: 20px; + + &__status { + color: $flw-muted; + font-size: 13px; + margin-left: auto; + } +} + +.flw-page-btn { + background: $flw-surface; + border: 1px solid $flw-border; + border-radius: $flw-radius; + color: $flw-ink; + cursor: pointer; + font-family: inherit; + font-size: 14px; + font-weight: 500; + min-height: 36px; + padding: 0 14px; + + &:disabled { + color: $flw-muted; + cursor: not-allowed; + } + + &:hover:not(:disabled) { + background: $flw-grey; + } +} + +.flw-empty { + color: $flw-body; + padding: 48px 24px; + text-align: center; + + p { + margin: 0 auto 16px; + max-width: 460px; + } + + &--hero { + border: 1px dashed $flw-border; + border-radius: 12px; + padding: 56px 24px; + + h2 { + font-size: 26px; + margin-bottom: 8px; + } + } + + &__art svg { + height: auto; + margin-bottom: 16px; + max-width: 160px; + } +} + +.flw-list-footnote { + color: $flw-muted; + font-size: 12px; + margin: 16px 0 0; +} + +/* Drawer & modal */ + +.flw-overlay { + background: rgba(16, 24, 40, 0.4); + display: flex; + inset: 0; + justify-content: flex-end; + position: fixed; + z-index: 100000; + + &--center { + align-items: center; + justify-content: center; + padding: 16px; + } +} + +.flw-drawer, +.flw-modal { + background: $flw-surface; + display: flex; + flex-direction: column; + + &__header { + align-items: center; + border-bottom: 1px solid $flw-line; + display: flex; + justify-content: space-between; + padding: 20px 24px; + + h2 { + font-size: 22px; + } + } + + &__body { + flex: 1; + overflow-y: auto; + padding: 24px; + } + + &__footer { + align-items: center; + border-top: 1px solid $flw-line; + display: flex; + gap: 12px; + justify-content: flex-end; + padding: 16px 24px; + + .flw-btn { + min-height: 40px; + padding: 0 18px; + } + } +} + +.flw-drawer { + height: 100%; + max-width: 100%; + width: 440px; + + &__amount { + font-family: $flw-serif; + font-size: 32px; + margin: 0 0 20px; + } + + &__body > .flw-pill { + margin-bottom: 12px; + } + + &__footer { + justify-content: space-between; + } +} + +.flw-modal { + border-radius: 16px; + box-shadow: $flw-shadow; + max-height: calc(100vh - 64px); + max-width: 100%; + width: 880px; +} + +.flw-icon-btn { + background: none; + border: 0; + border-radius: 6px; + color: $flw-body; + cursor: pointer; + font-size: 24px; + height: 32px; + line-height: 1; + width: 32px; + + &:hover { + background: $flw-grey; + } +} + +.flw-details { + margin: 0; +} + +.flw-detail { + border-bottom: 1px solid $flw-line; + display: grid; + gap: 12px; + grid-template-columns: 130px minmax(0, 1fr); + padding: 12px 0; + + dt { + color: $flw-muted; + } + + dd { + margin: 0; + overflow-wrap: anywhere; + } +} + +.flw-admin-root .flw-link-danger { + background: none; + border: 0; + color: #b42318; + cursor: pointer; + font-family: inherit; + font-size: 14px; + font-weight: 500; + padding: 0; + text-decoration: underline; + + &:disabled { + opacity: 0.6; + } +} + +/* Form builder */ + +.flw-builder__intro { + color: $flw-body; + margin: 0 0 16px; +} + +.flw-type-grid { + display: grid; + gap: 14px; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); +} + +.flw-type-card { + background: $flw-surface; + border: 1px solid $flw-line; + border-radius: 12px; + cursor: pointer; + display: flex; + flex-direction: column; + font-family: inherit; + gap: 6px; + padding: 20px; + text-align: left; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + + &:hover, + &:focus-visible { + border-color: $flw-orange; + box-shadow: 0 0 0 3px rgba(255, 155, 0, 0.15); + outline: none; + } + + &__icon { + align-items: center; + background: #fff3e0; + border-radius: $flw-radius; + color: $flw-orange-ink; + display: inline-flex; + height: 40px; + justify-content: center; + margin-bottom: 8px; + width: 40px; + + svg { + fill: currentcolor; + height: 22px; + width: 22px; + } + } + + &__title { + color: $flw-ink; + font-size: 15px; + font-weight: 500; + } + + &__desc { + color: $flw-body; + font-size: 13px; + } +} + +.flw-builder { + display: grid; + gap: 28px; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + + &__fields .flw-field-row { + gap: 14px; + } + + &__preview { + align-self: start; + background: #f9fafb; + border: 1px solid $flw-line; + border-radius: 12px; + min-height: 260px; + padding: 20px; + position: sticky; + top: 0; + } + + &__preview-label { + color: $flw-muted; + display: block; + font-size: 12px; + font-weight: 500; + letter-spacing: 0.04em; + margin-bottom: 14px; + text-transform: uppercase; + } + + &__preview-frame { + pointer-events: none; + } + + &__preview-note { + color: $flw-muted; + margin: 0; + } +} + +@media screen and (max-width: 782px) { + + .flw-builder { + grid-template-columns: minmax(0, 1fr); + } + + .flw-builder__preview { + position: static; + } + + .flw-popover { + left: 0; + right: auto; + } +} + + +/* Integrations */ + +.flw-integrations__lead { + color: $flw-body; + margin: 8px 0 0; + max-width: 640px; +} + +.flw-integrations__header { + align-items: flex-end; +} + +.flw-integrations__count { + background: #ecfdf3; + border-radius: 999px; + color: #027a48; + font-size: 13px; + font-weight: 500; + padding: 6px 14px; +} + +.flw-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 28px; +} + +.flw-admin-root .flw-section-title { + color: $flw-body; + font-family: $flw-sans; + font-size: 13px; + font-weight: 500; + letter-spacing: 0.04em; + margin: 0 0 14px; + text-transform: uppercase; +} + +.flw-integrations__grid { + display: grid; + gap: 20px; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + margin-bottom: 36px; + + &.is-loading { + animation: flw-pulse 1.2s ease-in-out infinite; + background: $flw-grey; + border-radius: 12px; + height: 260px; + } +} + +.flw-integration { + background: $flw-surface; + border: 1px solid $flw-line; + border-radius: 12px; + display: flex; + flex-direction: column; + padding: 22px; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + + &:not(.is-soon):hover { + border-color: $flw-border; + box-shadow: 0 4px 16px rgba(16, 24, 40, 0.06); + } + + &__head { + align-items: flex-start; + display: flex; + justify-content: space-between; + margin-bottom: 16px; + } + + &__logo { + align-items: center; + border-radius: 10px; + display: inline-flex; + font-size: 15px; + font-weight: 700; + height: 44px; + justify-content: center; + letter-spacing: 0.02em; + width: 44px; + } + + &__status { + border-radius: 999px; + font-size: 12px; + font-weight: 500; + padding: 3px 10px; + + &.is-active { + background: #ecfdf3; + color: #027a48; + } + + &.is-ready { + background: #fff3e0; + color: $flw-orange-ink; + } + + &.is-idle, + &.is-soon { + background: $flw-grey; + color: $flw-body; + } + } + + &__name { + color: $flw-ink; + font-size: 17px; + font-weight: 500; + margin: 0; + } + + &__category { + color: $flw-muted; + font-size: 13px; + } + + &__desc { + color: $flw-body; + flex: 1; + margin: 12px 0 0; + } + + &__hint { + color: $flw-muted; + font-size: 12px; + margin: 10px 0 0; + } + + &__warning { + background: #fffaeb; + border-radius: $flw-radius; + color: #b54708; + font-size: 13px; + margin: 12px 0 0; + padding: 8px 12px; + } + + &__foot { + align-items: center; + border-top: 1px solid $flw-line; + display: flex; + gap: 12px; + justify-content: space-between; + margin-top: 18px; + padding-top: 16px; + + .flw-btn { + min-height: 38px; + padding: 0 16px; + } + } + + &__blocked { + color: $flw-muted; + font-size: 13px; + } + + &.is-soon { + background: #fcfcfd; + + .flw-integration__logo { + filter: grayscale(1); + opacity: 0.6; + } + + .flw-integration__name, + .flw-integration__desc { + color: $flw-muted; + } + } +} + +.flw-admin-root .flw-integration__manage { + align-items: center; + color: $flw-ink; + display: inline-flex; + gap: 6px; + text-decoration: none; + + &:hover { + color: $flw-orange; + } + + svg { + height: 12px; + width: 12px; + } +} + +.flw-switch { + background: $flw-border; + border: 0; + border-radius: 999px; + cursor: pointer; + flex: none; + height: 24px; + padding: 0; + position: relative; + transition: background-color 0.15s ease; + width: 44px; + + &__thumb { + background: #fff; + border-radius: 50%; + box-shadow: 0 1px 3px rgba(16, 24, 40, 0.2); + height: 20px; + left: 2px; + position: absolute; + top: 2px; + transition: transform 0.15s ease; + width: 20px; + } + + &.is-on { + background: $flw-orange; + + .flw-switch__thumb { + transform: translateX(20px); + } + } + + &:focus-visible { + box-shadow: 0 0 0 3px rgba(255, 155, 0, 0.3); + outline: none; + } + + &:disabled { + cursor: wait; + opacity: 0.6; + } +} + + +/* Help menu and guided tips */ + +.flw-page__actions { + align-items: center; + display: flex; + gap: 8px; + margin-left: auto; +} + +.flw-help-btn { + align-items: center; + background: none; + border: 1px solid transparent; + border-radius: 999px; + color: $flw-body; + cursor: pointer; + display: inline-flex; + font-family: inherit; + font-size: 14px; + font-weight: 500; + gap: 6px; + padding: 6px 12px 6px 8px; + + &:hover, + &[aria-expanded="true"] { + background: $flw-grey; + color: $flw-ink; + } + + &:focus-visible { + box-shadow: 0 0 0 3px rgba(255, 155, 0, 0.3); + outline: none; + } +} + +.flw-help { + display: flex; + flex-direction: column; + gap: 4px; + margin: -8px; + width: 300px; + + &__item, + &__row { + align-items: center; + border-radius: $flw-radius; + display: flex; + gap: 16px; + justify-content: space-between; + padding: 10px 12px; + text-align: left; + } + + &__item { + background: none; + border: 0; + cursor: pointer; + flex-direction: column; + align-items: flex-start; + font-family: inherit; + gap: 2px; + + &:hover, + &:focus-visible { + background: $flw-grey; + outline: none; + } + } + + &__label { + color: $flw-ink; + display: block; + font-size: 14px; + font-weight: 500; + } + + &__hint { + color: $flw-muted; + display: block; + font-size: 13px; + } +} + +.flw-admin-root .flw-help__link { + align-items: center; + border-top: 1px solid $flw-line; + color: $flw-body; + display: flex; + font-size: 13px; + gap: 6px; + margin-top: 4px; + padding: 12px 12px 4px; + text-decoration: none; + + &:hover { + color: $flw-orange; + } + + svg { + height: 12px; + width: 12px; + } +} + +.flw-tour { + + &__blocker, + &__dim { + inset: 0; + position: fixed; + z-index: 100000; + } + + &__dim { + background: rgba(16, 24, 40, 0.5); + } + + &__spot { + border-radius: 10px; + box-shadow: 0 0 0 9999px rgba(16, 24, 40, 0.5); + pointer-events: none; + position: fixed; + transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease; + z-index: 100000; + } + + &__card { + background: $flw-surface; + border-radius: 12px; + box-shadow: 0 20px 48px rgba(16, 24, 40, 0.24); + color: $flw-ink; + max-width: calc(100vw - 32px); + padding: 20px 20px 16px; + position: fixed; + width: 340px; + z-index: 100001; + + &:focus { + outline: none; + } + } + + &__arrow { + background: $flw-surface; + height: 12px; + position: absolute; + transform: translateX(-50%) rotate(45deg); + width: 12px; + + .is-bottom & { + top: -6px; + } + + .is-top & { + bottom: -6px; + } + + .is-right & { + left: 0; + transform: translateY(-50%) rotate(45deg); + } + } + + &__count { + color: $flw-orange-ink; + display: block; + font-size: 12px; + font-weight: 500; + margin-bottom: 6px; + } + + .flw-admin-root &__title, + &__title { + font-family: $flw-sans; + font-size: 16px; + font-weight: 600; + line-height: 1.35; + margin: 0 0 6px; + } + + &__body { + color: $flw-body; + font-size: 14px; + line-height: 1.5; + margin: 0; + } + + &__foot { + align-items: center; + display: flex; + justify-content: space-between; + margin-top: 18px; + } + + &__nav { + display: flex; + gap: 8px; + + .flw-btn { + font-size: 14px; + min-height: 36px; + padding: 0 16px; + } + } + + &__skip { + background: none; + border: 0; + color: $flw-muted; + cursor: pointer; + font-family: inherit; + font-size: 13px; + padding: 4px 0; + + &:hover, + &:focus-visible { + color: $flw-ink; + text-decoration: underline; + } + } +} + +@media (prefers-reduced-motion: reduce) { + + .flw-tour__spot { + transition: none; + } +} + +@media screen and (max-width: 782px) { + + .flw-help-btn span { + display: none; + } +} + +/* -------------------------------------------------------------------------- */ + +/* Responsive */ + +/* -------------------------------------------------------------------------- */ + +@media screen and (max-width: 1100px) { + + .flw-page__body.has-aside { + grid-template-columns: minmax(0, 1fr); + } + + .flw-page__aside { + max-width: 720px; + order: -1; + } + +} + +@media screen and (max-width: 782px) { + + .flw-field-row { + grid-template-columns: minmax(0, 1fr); + } + + .flw-page__crumb-inner, + .flw-page__body { + padding-left: 16px; + padding-right: 16px; + } + + .flw-welcome__title { + font-size: 32px; + } + + .flw-method { + grid-template-columns: 18px minmax(0, 1fr); + + &__mark { + display: none; + } + } +} diff --git a/client/admin/app.js b/client/admin/app.js new file mode 100644 index 0000000..9820c24 --- /dev/null +++ b/client/admin/app.js @@ -0,0 +1,131 @@ +/** + * 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-payment-forms' + ), + } ); + 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 ), [] ); + + 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..a5f070a --- /dev/null +++ b/client/admin/components/index.js @@ -0,0 +1,403 @@ +/** + * 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. + * @param {*} [props.actions] Controls at the right of the breadcrumb bar. + * @return {Element} The page shell. + */ +export const Page = ( { + backLabel, + onBack, + children, + aside, + wide, + actions, +} ) => ( +
+
+
+ { onBack ? ( + + ) : ( + + + { backLabel } + + ) } + { actions && ( +
{ actions }
+ ) } +
+
+
+
+ { 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-payment-forms' ) + }{ ' ' } + + { current } { __( 'of', 'rave-payment-forms' ) } { 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 ) => ( +