From 1ac686eb0b2407eff60c958284fb58f5fe025f68 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 5 Jun 2024 10:38:56 +0100 Subject: [PATCH 01/12] bugfix - allow anonymous checkout via controller --- src/controllers/CheckoutController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/CheckoutController.php b/src/controllers/CheckoutController.php index 5ae148e..74691e6 100644 --- a/src/controllers/CheckoutController.php +++ b/src/controllers/CheckoutController.php @@ -22,6 +22,8 @@ */ class CheckoutController extends Controller { + protected array|bool|int $allowAnonymous = true; + /** * @inheritdoc */ From 56bf653ca61b6167e2e80025e9babd6944f87f1c Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 5 Jun 2024 10:39:53 +0100 Subject: [PATCH 02/12] bugfix - don't set customer email for null customer --- src/services/Checkout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/Checkout.php b/src/services/Checkout.php index a6e7824..99673bd 100644 --- a/src/services/Checkout.php +++ b/src/services/Checkout.php @@ -151,7 +151,7 @@ private function startCheckoutSession( if ($customer instanceof Customer) { $data['customer'] = $customer->stripeId; - } else { + } elseif ($customer !== null) { $data['customer_email'] = $customer; } From 53ed05bd7537e7484e39db396907352df2299397 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 5 Jun 2024 11:08:55 +0100 Subject: [PATCH 03/12] bugfix - readme tweaks --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 84cf113..f56a829 100644 --- a/README.md +++ b/README.md @@ -198,13 +198,13 @@ When a customer is ready to buy a product or start a subscription, you’ll prov Clicking a checkout link takes the customer to Stripe’s hosted checkout page, where they can complete a payment using whatever methods are available and enabled in your account. -To output a checkout link, use the `craft.stripeCheckoutUrl()` function: +To output a checkout link, use the `stripeCheckoutUrl()` function: ```twig {% set price = product.prices.one() %} {{ tag('a', { - href: craft.stripe.checkout.getCheckoutUrl( + href: stripeCheckoutUrl( [ { price: price.stripeId, @@ -416,7 +416,7 @@ You can set `$event->isValid` to prevent updates from being persisted during the ### Checkout Events -Customize the parameters sent to Stripe when generating a Checkout session by listening to the `craft\stripe\services\Checkout::EVENT_BEFORE_START_CHECKOUT_SESSION` event. The `craft\stripe\events\CheckoutSessionEvent` will have a `sessionData` property containing the request data that is about to be sent with the Stripe API client. You may modify or extend this data to suit your application—whatever the value of the property is after all handlers have been invoked is passed verbatim to the API client: +Customize the parameters sent to Stripe when generating a Checkout session by listening to the `craft\stripe\services\Checkout::EVENT_BEFORE_START_CHECKOUT_SESSION` event. The `craft\stripe\events\CheckoutSessionEvent` will have a `params` property containing the request data that is about to be sent with the Stripe API client. You may modify or extend this data to suit your application—whatever the value of the property is after all handlers have been invoked is passed verbatim to the API client: ```php craft\base\Event::on( @@ -433,11 +433,11 @@ craft\base\Event::on( if ($currentUser->isInGroup('members')) { // Memoize + assign values: - $data = $event->sourceData; + $data = $event->params; $data['metadata']['is_member'] = true; // Set back onto the event: - $event->sourceData = $data; + $event->params = $data; } }, ); From bbf6e82b8cca5a23e9809476e0737d1a34d23c84 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 5 Jun 2024 11:09:34 +0100 Subject: [PATCH 04/12] allow anonymous checkout --- README.md | 2 +- src/services/Checkout.php | 21 +++++++-------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f56a829..ac6eccf 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ To output a checkout link, use the `stripeCheckoutUrl()` function: quantity: 1, }, ], - null, + currentUser ? currentUser.email : null, 'shop/thank-you?session={CHECKOUT_SESSION_ID}', product.url, {} diff --git a/src/services/Checkout.php b/src/services/Checkout.php index 99673bd..273050b 100644 --- a/src/services/Checkout.php +++ b/src/services/Checkout.php @@ -51,23 +51,16 @@ public function getCheckoutUrl( ): string { $customer = null; - // if passed in user is a string - it should be an email address - if (is_string($user)) { + // if User element is passed in + if ($user instanceof User) { + // try to find the first Stripe Customer for that User's email + // if none is found just use the User's email we have on account + $customer = $this->getCheckoutCustomerByEmail($user->email) ?? $user->email; + } else if (is_string($user)) { + // if passed in user is a string - it should be an email address; // try to find the first Stripe Customer for this email; // if none is found just use the email that was passed in $customer = $this->getCheckoutCustomerByEmail($user) ?? $user; - } else { - // if user is null - try to get the currently logged in user - if ($user === null) { - $user = Craft::$app->getUser()->getIdentity(); - } - - // if User element is passed in, or we just got one via getIdentity - if ($user instanceof User) { - // try to find the first Stripe Customer for that User's email - // if none is found just use the User's email we have on account - $customer = $this->getCheckoutCustomerByEmail($user->email) ?? $user->email; - } } return $this->startCheckoutSession( From db5bf1a5eb65653b5717ded9534d5f15c3298f36 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 5 Jun 2024 11:26:02 +0100 Subject: [PATCH 05/12] ecs --- src/services/Checkout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/Checkout.php b/src/services/Checkout.php index 273050b..332c65b 100644 --- a/src/services/Checkout.php +++ b/src/services/Checkout.php @@ -56,7 +56,7 @@ public function getCheckoutUrl( // try to find the first Stripe Customer for that User's email // if none is found just use the User's email we have on account $customer = $this->getCheckoutCustomerByEmail($user->email) ?? $user->email; - } else if (is_string($user)) { + } elseif (is_string($user)) { // if passed in user is a string - it should be an email address; // try to find the first Stripe Customer for this email; // if none is found just use the email that was passed in From 21252de4fe04e1189fb420df1a92888fcfbf32ea Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 5 Jun 2024 11:53:25 +0100 Subject: [PATCH 06/12] stripeCheckoutUrl - of course customer can be User element too --- README.md | 2 +- src/web/twig/Extension.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ac6eccf..fa1c608 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ To output a checkout link, use the `stripeCheckoutUrl()` function: quantity: 1, }, ], - currentUser ? currentUser.email : null, + currentUser, 'shop/thank-you?session={CHECKOUT_SESSION_ID}', product.url, {} diff --git a/src/web/twig/Extension.php b/src/web/twig/Extension.php index 9cd1624..143cd81 100644 --- a/src/web/twig/Extension.php +++ b/src/web/twig/Extension.php @@ -7,6 +7,7 @@ namespace craft\stripe\web\twig; +use craft\elements\User; use craft\stripe\helpers\Price; use craft\stripe\Plugin; use Twig\Extension\AbstractExtension; @@ -46,7 +47,7 @@ public function getFunctions() return [ new TwigFunction('stripeCheckoutUrl', function( array $lineItems = [], - ?string $customer = null, + string|User|null $customer = null, ?string $successUrl = null, ?string $cancelUrl = null, ?array $params = null, From bef4661466d856081bac7f9d8e9d5f1c7e365282 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Fri, 14 Jun 2024 10:01:29 +0100 Subject: [PATCH 07/12] getCheckoutUrl back to accepting false and keeping null the same --- README.md | 9 +++++++++ src/controllers/CheckoutController.php | 11 ++++++++-- src/services/Checkout.php | 28 +++++++++++++++++--------- src/web/twig/Extension.php | 2 +- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index fa1c608..81d6d4a 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,9 @@ To output a checkout link, use the `stripeCheckoutUrl()` function: }) }} ``` +> [!TIP] +> To allow anonymous checkout, you can pass `false` as the second parameter to the `stripeCheckoutUrl()`. + ### Checkout Form As an alternative to generating static Checkout links, you can build a [form](https://craftcms.com/docs/5.x/development/forms.html) that sends a list of items and other params to Craft, which will create a checkout session on-the-fly, then redirect the customer to the Stripe-hosted checkout page: @@ -245,6 +248,12 @@ As an alternative to generating static Checkout links, you can build a [form](ht ``` +> [!TIP] +> By default, `stripe/checkout` action will be attempted for the currently logged-in user. +> +> If you'd like to allow anonymous checkout, even when user is logged in, you can add `{{ hiddenInput('customer', 'false') }}` to the form. + + ### Billing Portal Customers can manage their subscriptions and payment methods via Stripe’s hosted [billing portal](https://docs.stripe.com/customer-management). You can generate a URL to a customer’s billing portal using the `currentUser.getStripeBillingPortalSessionUrl()` method: diff --git a/src/controllers/CheckoutController.php b/src/controllers/CheckoutController.php index 74691e6..3a61a70 100644 --- a/src/controllers/CheckoutController.php +++ b/src/controllers/CheckoutController.php @@ -42,8 +42,6 @@ public function actionCheckout(): Response $request = Craft::$app->getRequest(); - $currentUser = Craft::$app->getUser()->getIdentity(); - // process line items $postLineItems = $request->getRequiredBodyParam('lineItems'); $lineItems = collect($postLineItems)->filter(fn($item) => $item['quantity'] > 0)->all(); @@ -54,6 +52,15 @@ public function actionCheckout(): Response $successUrl = $request->getValidatedBodyParam('successUrl'); $cancelUrl = $request->getValidatedBodyParam('cancelUrl'); + $customer = $request->getBodyParam('customer'); + + if ($customer == 'false' || $customer == '0' || $customer === false) { + // if customer was explicitly set to something falsy, + // go with false to prevent trying to find the currently logged in user further down the line + $currentUser = false; + } else { + $currentUser = $customer; + } // start checkout session $url = Plugin::getInstance()->getCheckout()->getCheckoutUrl($lineItems, $currentUser, $successUrl, $cancelUrl); diff --git a/src/services/Checkout.php b/src/services/Checkout.php index 332c65b..0936c36 100644 --- a/src/services/Checkout.php +++ b/src/services/Checkout.php @@ -37,30 +37,38 @@ class Checkout extends Component * Returns checkout URL based on the provided email. * * @param array $lineItems - * @param string|User|null $user User Element or email address + * @param string|User|false|null $user User Element or email address * @param string|null $successUrl * @param string|null $cancelUrl + * @param array|null $params * @return string */ public function getCheckoutUrl( array $lineItems = [], - string|User|null $user = null, + string|User|false|null $user = null, ?string $successUrl = null, ?string $cancelUrl = null, ?array $params = null, ): string { $customer = null; - // if User element is passed in - if ($user instanceof User) { - // try to find the first Stripe Customer for that User's email - // if none is found just use the User's email we have on account - $customer = $this->getCheckoutCustomerByEmail($user->email) ?? $user->email; - } elseif (is_string($user)) { - // if passed in user is a string - it should be an email address; + // if passed in user is a string - it should be an email address + if (is_string($user)) { // try to find the first Stripe Customer for this email; // if none is found just use the email that was passed in $customer = $this->getCheckoutCustomerByEmail($user) ?? $user; + } else { + // if user is null - try to get the currently logged in user + if ($user === null) { + $user = Craft::$app->getUser()->getIdentity(); + } + + // if User element is passed in, or we just got one via getIdentity + if ($user instanceof User) { + // try to find the first Stripe Customer for that User's email + // if none is found just use the User's email we have on account + $customer = $this->getCheckoutCustomerByEmail($user->email) ?? $user->email; + } } return $this->startCheckoutSession( @@ -144,7 +152,7 @@ private function startCheckoutSession( if ($customer instanceof Customer) { $data['customer'] = $customer->stripeId; - } elseif ($customer !== null) { + } elseif (is_string($customer)) { $data['customer_email'] = $customer; } diff --git a/src/web/twig/Extension.php b/src/web/twig/Extension.php index 143cd81..d8f54bd 100644 --- a/src/web/twig/Extension.php +++ b/src/web/twig/Extension.php @@ -47,7 +47,7 @@ public function getFunctions() return [ new TwigFunction('stripeCheckoutUrl', function( array $lineItems = [], - string|User|null $customer = null, + string|User|false|null $customer = null, ?string $successUrl = null, ?string $cancelUrl = null, ?array $params = null, From 56f600d1568f43a6d359751c4afb5ea861983488 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Fri, 14 Jun 2024 10:01:49 +0100 Subject: [PATCH 08/12] allow parameters for price.getCheckoutUrl() --- src/elements/Price.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/elements/Price.php b/src/elements/Price.php index c548510..6427986 100644 --- a/src/elements/Price.php +++ b/src/elements/Price.php @@ -606,11 +606,20 @@ public function getProduct(): Product|null /** * Shortcut to get the checkout URL for the price element. * + * @param string|User|false|null $customer, + * @param string|null $successUrl, + * @param string|null $cancelUrl, + * @param array|null $params, * @return string * @throws InvalidConfigException * @throws \Throwable */ - public function getCheckoutUrl(): string + public function getCheckoutUrl( + string|User|false|null $customer = null, + ?string $successUrl = null, + ?string $cancelUrl = null, + ?array $params = null, + ): string { return Plugin::getInstance()->getCheckout()->getCheckoutUrl( [ @@ -619,7 +628,10 @@ public function getCheckoutUrl(): string 'quantity' => 1, ], ], - Craft::$app->getUser()->getIdentity(), + $customer, + $successUrl, + $cancelUrl, + $params ); } } From 3f50c57378f0bae2c35fe4467306188719695321 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Fri, 14 Jun 2024 13:09:34 +0100 Subject: [PATCH 09/12] prettier --- .github/workflows/ci.yml | 2 +- .prettierignore | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .prettierignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7886e41..66f549c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: name: ci uses: craftcms/.github/.github/workflows/ci.yml@v3 with: - craft_version: '5' + craft_version: "5" jobs: '["ecs", "phpstan", "prettier"]' notify_slack: true slack_subteam: diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..0ba6ef0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +src/web/assets/stripecp/dist +composer.lock +README.md \ No newline at end of file From bd1f1405477775604fbd5d8bf60119da5b513bdb Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Fri, 14 Jun 2024 13:09:41 +0100 Subject: [PATCH 10/12] tweaks --- src/controllers/CheckoutController.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/controllers/CheckoutController.php b/src/controllers/CheckoutController.php index 3a61a70..6944281 100644 --- a/src/controllers/CheckoutController.php +++ b/src/controllers/CheckoutController.php @@ -54,16 +54,14 @@ public function actionCheckout(): Response $cancelUrl = $request->getValidatedBodyParam('cancelUrl'); $customer = $request->getBodyParam('customer'); - if ($customer == 'false' || $customer == '0' || $customer === false) { + if ($customer == 'false' || $customer == '0' || $customer === false || $customer === 0) { // if customer was explicitly set to something falsy, // go with false to prevent trying to find the currently logged in user further down the line - $currentUser = false; - } else { - $currentUser = $customer; + $customer = false; } // start checkout session - $url = Plugin::getInstance()->getCheckout()->getCheckoutUrl($lineItems, $currentUser, $successUrl, $cancelUrl); + $url = Plugin::getInstance()->getCheckout()->getCheckoutUrl($lineItems, $customer, $successUrl, $cancelUrl); return $this->redirect($url); } From b86cd74b79d9e5b5bbf47f24a46252e3187d9cc7 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Fri, 14 Jun 2024 13:09:50 +0100 Subject: [PATCH 11/12] readme update --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 81d6d4a..2986717 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,12 @@ The Stripe plugin handles this relationship using [nested elements](https://craf {{ price.data|unitAmount }} {{ tag('a', { text: "Buy now", - href: price.getCheckoutUrl(), + href: price.getCheckoutUrl( + currentUser ?? false, + 'shop/thank-you?session={CHECKOUT_SESSION_ID}', + product.url, + {} + ), }) }} {% endfor %} @@ -211,7 +216,7 @@ To output a checkout link, use the `stripeCheckoutUrl()` function: quantity: 1, }, ], - currentUser, + currentUser ?? false, 'shop/thank-you?session={CHECKOUT_SESSION_ID}', product.url, {} @@ -221,7 +226,7 @@ To output a checkout link, use the `stripeCheckoutUrl()` function: ``` > [!TIP] -> To allow anonymous checkout, you can pass `false` as the second parameter to the `stripeCheckoutUrl()`. +> Passing `false` as the second parameter to the `stripeCheckoutUrl()` allows you to create an anonymous checkout URL. ### Checkout Form @@ -235,6 +240,9 @@ As an alternative to generating static Checkout links, you can build a [form](ht {{ actionInput('stripe/checkout') }} {{ hiddenInput('successUrl', 'shop/thank-you?session={CHECKOUT_SESSION_ID}'|hash) }} {{ hiddenInput('cancelUrl', 'shop'|hash) }} + {% if not currentUser %} + {{ hiddenInput('customer', 'false') }} + {% endif %}