From 67007fc2664ab87a2573be95dd9a314076bdf65d Mon Sep 17 00:00:00 2001 From: Jaafar Date: Thu, 6 Aug 2026 12:49:25 +0300 Subject: [PATCH] feat: embedded iframe checkout option (one-off payments) Add an "Embedded Checkout" gateway option that renders the GetPayIn hosted checkout in an iframe instead of redirecting to it. When enabled, the plugin sends iframe=1 on the v2 init call so the returned Laravel-signed checkout_url carries the flag (appending it to the URL would break the signed middleware), embeds that URL, and listens for the checkout's signed paylink_payment postMessage to move the top window to the shop's return (success) or error (failure) URL. The listener accepts messages only from the configured GetPayIn origin, so a foreign frame cannot spoof an outcome. Applies to one-off payments only; recurring subscriptions always redirect because the recurring init endpoint does not sign the iframe flag. Bumps version to 1.3.0 and documents the option in the README, CHANGELOG, and the .pot translation template. --- CHANGELOG.md | 6 +++ README.md | 7 ++- languages/vikpaylink.pot | 8 ++- paylink.php | 108 +++++++++++++++++++++++++++++++++++++++ vikpaylink.php | 4 +- 5 files changed, 129 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f84e5a6..dc731a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.0] + +### Added + +- **Embedded checkout (iframe)** — a new gateway option that shows the GetPayIn checkout inside your site in an iframe instead of redirecting to it. When enabled, the plugin sends `iframe=1` on the v2 `init` call so the returned (Laravel-signed) `checkout_url` carries the flag, embeds it, and listens for the checkout's signed `paylink_payment` postMessage to move the top window to the shop's return (success) or error (failure) URL. The listener accepts messages only from the configured GetPayIn origin, so a foreign frame cannot spoof an outcome. Applies to one-off payments only; recurring subscriptions always redirect (the recurring init endpoint does not sign the flag). + ## [1.2.1] ### Fixed diff --git a/README.md b/README.md index 5602909..ec4dcc1 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Works with all five Vik plugins: ## Features - Hosted checkout redirect (`beginTransaction` → GetPayIn) with idempotent invoice creation +- Optional **embedded checkout** — render the hosted checkout in an iframe on your site (one-off payments), with a signed `postMessage` return - Capture now, or **authorize** and capture later from the dashboard - Fixed **installments** (2–24) on the hosted checkout - **Recurring subscriptions** — creates a mandate and charges the order total every cycle @@ -62,6 +63,7 @@ the **GetPayIn** gateway, and fill in: | **Hash token** | Your integration's signing secret. Used server-side only to sign requests and verify webhooks — never exposed to the browser. | | **Base URL** | GetPayIn host. Defaults to `https://pay.getpayin.com`. | | **Payment action** | `Capture` (charge immediately) or `Authorize` (hold now, capture later). | +| **Embedded checkout** | `Yes` embeds the GetPayIn checkout in an iframe on your site instead of redirecting to it; `No` (default) redirects. Requires your integration **Origin** to exactly match this site's URL. One-off payments only — recurring subscriptions always redirect. | | **Installments** | `Yes` to offer fixed installments, with the **Number of installments** (2–24). Requires installments enabled on your account. | | **Payment type** | `One-off` for a single payment, or `Recurring subscription` to create a mandate. | | **Recurring interval / count / total cycles / consent text** | For recurring: the billing period (`month`/`week`/`day`/`year`), how many intervals between charges, an optional cap on the number of charges, and the consent statement shown to the payer. | @@ -78,7 +80,10 @@ the **GetPayIn** gateway, and fill in: order. One-off payments go to `{base_url}/api/v2/integration/init`; recurring payments go to `{base_url}/api/v2/integration/recurring/init` (which also returns a `mandate_id`). The customer is redirected to the returned - `checkout_url`. + `checkout_url` — or, with **Embedded checkout** enabled for a one-off payment, + the plugin sends `iframe=1` on `init` and renders that `checkout_url` in an + iframe, then moves the top window to the return/error URL when the checkout + posts its signed `paylink_payment` message. 2. **Payment** — The customer pays on GetPayIn's hosted checkout. 3. **Confirmation** — GetPayIn calls the plugin's webhook. The plugin verifies the body signature (fail-closed) and, on `success=1` with a paid/authorized status, diff --git a/languages/vikpaylink.pot b/languages/vikpaylink.pot index 5a439e8..318794f 100644 --- a/languages/vikpaylink.pot +++ b/languages/vikpaylink.pot @@ -2,7 +2,7 @@ # This file is distributed under the GPL-2.0-or-later license. msgid "" msgstr "" -"Project-Id-Version: GetPayIn for VikWP 1.2.0\n" +"Project-Id-Version: GetPayIn for VikWP 1.3.0\n" "Report-Msgid-Bugs-To: https://github.com/GetPayin-Tech/paylink-vikwp/issues\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -19,6 +19,9 @@ msgstr "" msgid "Consent Text//Shown to the payer when they authorise recurring charges. Required for recurring payments." msgstr "" +msgid "Embedded Checkout//Show the GetPayIn checkout embedded in your site (iframe) instead of redirecting to it. Your integration Origin must exactly match this site's URL. One-off payments only — recurring subscriptions always redirect." +msgstr "" + msgid "Hash Token//The signing secret from the same screen. It is used to sign requests and verify webhooks, and never leaves your server." msgstr "" @@ -55,6 +58,9 @@ msgstr "" msgid "Total Cycles//Optional. Stop after this many charges. Leave empty for an open-ended subscription." msgstr "" +msgid "Trouble viewing the checkout? Open it in this window" +msgstr "" + msgid "We could not start the GetPayIn checkout. Please try again or contact the store." msgstr "" diff --git a/paylink.php b/paylink.php index 6c86079..3e47bdd 100644 --- a/paylink.php +++ b/paylink.php @@ -94,6 +94,11 @@ protected function buildAdminParameters() 'type' => 'select', 'options' => array('Capture', 'Authorize'), ), + 'iframe_enabled' => array( + 'label' => __('Embedded Checkout//Show the GetPayIn checkout embedded in your site (iframe) instead of redirecting to it. Your integration Origin must exactly match this site\'s URL. One-off payments only — recurring subscriptions always redirect.', 'vikpaylink'), + 'type' => 'select', + 'options' => array('No', 'Yes'), + ), 'installments_enabled' => array( 'label' => __('Installments//Offer fixed installments on the GetPayIn checkout. Requires installments enabled on your account.', 'vikpaylink'), 'type' => 'select', @@ -147,6 +152,23 @@ protected function beginTransaction() return; } + if ($this->iframeEnabled()) { + $this->renderIframe($checkoutUrl); + } else { + $this->renderRedirect($checkoutUrl); + } + } + + /** + * Renders the full-page redirect to the hosted GetPayIn checkout, with a "Pay Now" + * link as the no-JavaScript fallback. + * + * @param string $checkoutUrl The hosted checkout URL. + * + * @return void + */ + protected function renderRedirect($checkoutUrl) + { $safeUrl = htmlspecialchars($checkoutUrl, ENT_QUOTES, 'UTF-8'); echo ''; } + /** + * Embeds the hosted GetPayIn checkout in an iframe and listens for the signed + * `paylink_payment` postMessage the checkout sends on completion, then moves the top + * window to the shop's return (success) or error (failure) URL. The listener accepts + * messages only from the configured GetPayIn origin, so a foreign frame cannot spoof + * an outcome. A plain link is shown for browsers that block third-party frames. + * + * @param string $checkoutUrl The hosted checkout URL (already carries `iframe=1`). + * + * @return void + */ + protected function renderIframe($checkoutUrl) + { + $safeUrl = htmlspecialchars($checkoutUrl, ENT_QUOTES, 'UTF-8'); + + $returnUrl = (string) $this->get('return_url', ''); + $errorUrl = (string) $this->get('error_url', ''); + + if ($errorUrl === '') { + $errorUrl = $returnUrl; + } + + echo ''; + + echo ''; + } + /** * Verifies the signed GetPayIn webhook and reports the payment status to Vik. * @@ -244,6 +312,10 @@ protected function createCheckoutUrl() $body['installments'] = (string) $this->installmentCount(); } + if ($this->iframeEnabled()) { + $body['iframe'] = '1'; + } + $response = $this->httpPost($this->apiBaseUrl() . '/api/v2/integration/init', $body); $checkoutUrl = $this->checkoutUrlFrom($response); @@ -472,6 +544,30 @@ protected function apiBaseUrl() return rtrim($url, '/'); } + /** + * The scheme://host[:port] origin of the GetPayIn checkout, used to authenticate the + * `postMessage` sent by the embedded checkout. Returns an empty string when the base + * URL cannot be parsed, in which case the listener skips the origin check. + * + * @return string + */ + protected function checkoutOrigin() + { + $parts = parse_url($this->apiBaseUrl()); + + if (empty($parts['scheme']) || empty($parts['host'])) { + return ''; + } + + $origin = $parts['scheme'] . '://' . $parts['host']; + + if (!empty($parts['port'])) { + $origin .= ':' . $parts['port']; + } + + return $origin; + } + /** * Formats a monetary amount to a fixed 2-decimal wire form. * @@ -604,6 +700,18 @@ protected function isRecurring() return $this->getParam('paymenttype') === 'Recurring subscription'; } + /** + * Whether the checkout should be embedded in an iframe rather than redirected to. + * Only one-off payments support the embedded flow; the recurring init endpoint does + * not sign an iframe flag, so recurring subscriptions always redirect. + * + * @return bool + */ + protected function iframeEnabled() + { + return $this->getParam('iframe_enabled') === 'Yes' && !$this->isRecurring(); + } + /** * The fixed installment count, clamped to the GetPayIn-supported 2–24 range. * diff --git a/vikpaylink.php b/vikpaylink.php index 3866e4f..b887836 100644 --- a/vikpaylink.php +++ b/vikpaylink.php @@ -3,7 +3,7 @@ Plugin Name: GetPayIn for VikWP Plugin URI: https://github.com/GetPayin-Tech/paylink-vikwp Description: GetPayIn integration to collect payments through the Vik plugins (VikBooking, VikRentCar, VikRentItems, VikAppointments, VikRestaurants). -Version: 1.2.1 +Version: 1.3.0 Requires at least: 5.6 Requires PHP: 7.2 Author: GetPayIn @@ -23,7 +23,7 @@ define('VIKPAYLINK_LANG', basename(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'languages'); -define('VIKPAYLINKVERSION', '1.2.1'); +define('VIKPAYLINKVERSION', '1.3.0'); add_action('init', function () { JFactory::getLanguage()->load('vikpaylink', VIKPAYLINK_LANG);