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
90 changes: 82 additions & 8 deletions inc/render/class-stripe-checkout-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
* Class Stripe_Checkout_Block
*/
class Stripe_Checkout_Block {
/**
* Transient prefix for the cached checkout mode of a price.
*
* @var string
*/
const PRICE_MODE_CACHE_PREFIX = 'otter_stripe_price_mode_';

/**
* Stripe API instance.
*
Expand Down Expand Up @@ -56,8 +63,8 @@ public function watch_checkout() {

$product_id = isset( $_GET['product_id'] ) ? sanitize_text_field( wp_unslash( $_GET['product_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$price_id = isset( $_GET['price_id'] ) ? sanitize_text_field( wp_unslash( $_GET['price_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$url = isset( $_GET['url'] ) ? sanitize_text_field( wp_unslash( $_GET['url'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$mode = isset( $_GET['mode'] ) ? sanitize_text_field( wp_unslash( $_GET['mode'] ) ) : 'payment'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$url = isset( $_GET['url'] ) ? sanitize_url( wp_unslash( $_GET['url'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

if ( empty( $product_id ) || empty( $price_id ) || empty( $url ) ) {
return sprintf(
Expand All @@ -66,12 +73,19 @@ public function watch_checkout() {
);
}

if ( ! hash_equals( self::get_checkout_token( $product_id, $price_id ), $token ) ) {
return sprintf(
'<div class="wp-block-themeisle-blocks-stripe-checkout"><div class="o-stripe-checkout">%s</div></div>',
__( 'An error occurred! Could not retrieve the product information!', 'otter-blocks' )
);
}

$permalink = add_query_arg(
array(
'stripe_session_id' => '{CHECKOUT_SESSION_ID}',
'product_id' => $product_id,
),
$url
$this->get_return_url( $url )
);

$session = $this->stripe_api->create_request(
Expand All @@ -85,7 +99,7 @@ public function watch_checkout() {
'quantity' => 1,
),
),
'mode' => $mode,
'mode' => $this->get_mode_for_price( $price_id ),
)
);

Expand Down Expand Up @@ -165,17 +179,22 @@ public function render( $attributes ) {
$details_markup .= '<h5>' . $currency . $amount . '</h5>';
$details_markup .= '</div>';

$mode = 'recurring' === $price['type'] ? 'subscription' : 'payment';
// A widget area or an FSE template has no permalink of its own, so fall back to the current URL.
$return_url = get_permalink();

if ( ! is_string( $return_url ) || '' === $return_url ) {
$return_url = home_url( add_query_arg( array() ) );
}

$session_url = add_query_arg(
array(
'action' => 'buy_stripe',
'product_id' => $attributes['product'],
'price_id' => $attributes['price'],
'url' => get_permalink(),
'mode' => $mode,
'url' => $return_url,
'token' => self::get_checkout_token( $attributes['product'], $attributes['price'] ),
),
get_permalink()
$return_url
);

$button_markup = '<a href="' . esc_url( $session_url ) . '">' . __( 'Checkout', 'otter-blocks' ) . '</a>';
Expand All @@ -188,6 +207,61 @@ public function render( $attributes ) {
);
}

/**
* Sign a product/price pair so the checkout can verify it was offered by a block.
*
* @param string $product_id Stripe product ID.
* @param string $price_id Stripe price ID.
* @return string
*/
public static function get_checkout_token( $product_id, $price_id ) {
return hash_hmac( 'sha256', $product_id . '|' . $price_id, wp_salt( 'otter_stripe' ) );
}

/**
* Get the URL Stripe returns the buyer to, restricted to this site.
*
* @param string $url Requested return URL.
* @return string
*/
private function get_return_url( $url ) {
$host = wp_parse_url( $url, PHP_URL_HOST );
$home = wp_parse_url( home_url(), PHP_URL_HOST );

if ( $home !== $host ) {
return home_url( '/' );
}

return $url;
}

/**
* Get the checkout session mode for a price.
*
* @param string $price_id Stripe price ID.
* @return string
*/
private function get_mode_for_price( $price_id ) {
$cache_key = self::PRICE_MODE_CACHE_PREFIX . md5( $price_id );
$cached = get_transient( $cache_key );

if ( 'payment' === $cached || 'subscription' === $cached ) {
return $cached;
}

$price = $this->stripe_api->create_request( 'price', $price_id );

if ( is_wp_error( $price ) || ! isset( $price['type'] ) ) {
return 'payment';
}

$mode = 'recurring' === $price['type'] ? 'subscription' : 'payment';

set_transient( $cache_key, $mode, WEEK_IN_SECONDS );

return $mode;
}

/**
* Format the error message.
*
Expand Down
4 changes: 2 additions & 2 deletions src/blocks/test/e2e/blocks/tabs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ test.describe( 'Tabs Block', () => {
await admin.createNewPost();
});

test( 'can be created by typing "/tabs"', async({ editor, page }) => {
test( 'can be created by typing "/themeisle-tabs"', async({ editor, page }) => {

// Create a Progress Block with the slash block shortcut.
await insertBlockBySlash({
editor,
page,
shortcut: '/tabs',
shortcut: '/themeisle-tabs',
blockName: 'themeisle-blocks/tabs'
});
});
Expand Down
33 changes: 33 additions & 0 deletions tests/stripe-http-client-mock.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,43 @@

class StripeHttpClientMock implements ClientInterface
{
/**
* Paths requested since the last reset, so tests can assert on API usage.
*
* @var array
*/
public static $request_paths = array();

/**
* Parameters of the requests made since the last reset, keyed by path.
*
* @var array
*/
public static $request_params = array();

public static function reset_request_paths()
{
self::$request_paths = array();
}

public static function reset_request_params()
{
self::$request_params = array();
}

public static function get_params_for($path)
{
return isset(self::$request_params[$path]) ? self::$request_params[$path] : array();
}

public function request($method, $absUrl, $headers, $params, $hasFile)
{
$urlParts = parse_url($absUrl);
$path = $urlParts['path'];

self::$request_paths[] = $path;
self::$request_params[$path] = $params;

if ($path === '/v1/products') {
return array($this->mockProductsList(), 200, null);
}
Expand Down Expand Up @@ -101,6 +133,7 @@ private function mockSingleProduct()
'price' => 1200,
'currency' => 'USD',
'active' => true,
'images' => [],
'object' => 'product'
]
);
Expand Down
Loading
Loading