Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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. |
Expand All @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion languages/vikpaylink.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 ""

Expand Down Expand Up @@ -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 ""

Expand Down
108 changes: 108 additions & 0 deletions paylink.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 '<div class="vikpaylink-redirect" style="text-align:center;">'
Expand All @@ -156,6 +178,52 @@ protected function beginTransaction()
. '</div>';
}

/**
* 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 '<div class="vikpaylink-iframe-wrap" style="width:100%;">'
. '<iframe class="vikpaylink-iframe" src="' . $safeUrl . '" '
. 'style="width:100%;min-height:640px;height:82vh;border:0;" '
. 'allow="payment *" allowpaymentrequest="true"></iframe>'
. '<p class="vikpaylink-iframe-fallback" style="text-align:center;">'
. '<a class="btn btn-primary vikpaylink-paynow" href="' . $safeUrl . '" target="_top">'
. __('Trouble viewing the checkout? Open it in this window', 'vikpaylink') . '</a></p>'
. '</div>';

echo '<script>(function(){'
. 'var expected=' . json_encode($this->checkoutOrigin()) . ';'
. 'var ret=' . json_encode($returnUrl) . ';'
. 'var err=' . json_encode($errorUrl) . ';'
. 'window.addEventListener("message",function(e){'
. 'if(expected&&e.origin!==expected){return;}'
. 'var d=e.data;'
. 'if(!d||typeof d!=="object"||d.type!=="paylink_payment"){return;}'
. 'var url=d.success?ret:err;'
. 'try{if(url){window.top.location.href=url;}else{window.top.location.reload();}}'
. 'catch(ex){if(url){window.location.href=url;}else{window.location.reload();}}'
. '});'
. '}());</script>';
}

/**
* Verifies the signed GetPayIn webhook and reports the payment status to Vik.
*
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
4 changes: 2 additions & 2 deletions vikpaylink.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down