Skip to content

Commit

Permalink
Merge branch 'release/4.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
nfourtythree committed Dec 19, 2023
2 parents 398095f + e49f702 commit a82824f
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 13 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Release Notes for Stripe for Craft Commerce

## 4.1.0 - 2023-12-19

- Stripe for Craft Commerce now requires Commerce 4.3.3 or later.
- It is now possible to create SEPA and Bacs Direct Debit payment sources.
- Payment method data is now stored in expanded form within transaction response data. ([#276](https://github.com/craftcms/commerce-stripe/pull/276))
- Billing address information is now passed to the payment intent. ([#257](https://github.com/craftcms/commerce-stripe/issues/257), [#258](https://github.com/craftcms/commerce-stripe/issues/263))
- Fixed a bug where it wasn’t possible to pay using the SEPA Direct Debit payment method. ([#265](https://github.com/craftcms/commerce/issues/265))
- Fixed a bug where failed PayPal payments would cause infinite redirects. ([#266](https://github.com/craftcms/commerce-stripe/issues/266))
- Fixed a bug where JavaScript files were being served incorrectly. ([#270](https://github.com/craftcms/commerce-stripe/issues/270))
- Added `craft\commerce\stripe\SubscriptionGateway::handlePaymentIntentSucceeded()`.

## 4.0.1.1 - 2023-10-25

- Restored support for backend payments using the old payment form.
Expand Down Expand Up @@ -27,7 +38,7 @@
- Removed `craft\commerce\stripe\base\Gateway::normalizePaymentToken()`.
- Removed `craft\commerce\stripe\events\BuildGatewayRequestEvent::$metadata`. `BuildGatewayRequestEvent::$request` should be used instead.
- Deprecated the `commerce-stripe/default/fetch-plans` action.
- Deprecated creating new payment sources via the `commerce/subscriptions/subscribe` action.
- Deprecated creating new payment sources via the `commerce/subscriptions/subscribe` action.
- Fixed a bug where `craft\commerce\stripe\base\SubscriptionGateway::getSubscriptionPlans()` was returning incorrectly-formatted data.

## 3.1.1 - 2023-05-10
Expand Down
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ This plugin provides a [gateway](https://craftcms.com/docs/commerce/4.x/payment-
## Requirements

- Craft CMS 4.0 or later
- Craft Commerce 4.3 or later
- Craft Commerce 4.3.3 or later
- Stripe [API version](https://stripe.com/docs/api/versioning) `2022-11-15`

## Installation
Expand Down Expand Up @@ -490,6 +490,38 @@ The default `elementOptions` value only defines a layout:
} %}
```
### `order` (optional)
The `order` key should be a reference to a Commerce `Order` model, which would usually be the current `cart` variable in your template.
If supplied, the [billing details](https://stripe.com/docs/js/elements_object/create_payment_element#payment_element_create-options-defaultValues) are added to `elementOptions`’s default `defaultValues` array.
```twig
{% set params = {
order: cart,
} %}
{{ cart.gateway.getPaymentFormHtml(params)|raw }}
```
If you do not pass an `order` into the payment form, you can opt to manually populate the [billing details](https://stripe.com/docs/js/elements_object/create_payment_element#payment_element_create-options-defaultValues) using the `elementOptions`’s `defaultValues` key:
```twig
{% set params = {
elementOptions: {
defaultValues: {
name: 'Jane Doe',
address: {
line1: '123 Main St',
city: 'Anytown',
state: 'NY',
postal_code: '12345',
country: 'US',
},
}
...
```
### `errorMessageClasses`
Error messages are displayed in a container above the form. You can add classes to this element to alter its style.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"php": "^8.0.2",
"craftcms/cms": "^4.0.0",
"stripe/stripe-php": "^10.0",
"craftcms/commerce": "^4.3"
"craftcms/commerce": "^4.3.3"
},
"require-dev": {
"craftcms/phpstan": "dev-main",
Expand Down
55 changes: 53 additions & 2 deletions src/base/SubscriptionGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use craft\commerce\models\subscriptions\SubscriptionForm as BaseSubscriptionForm;
use craft\commerce\models\subscriptions\SubscriptionPayment;
use craft\commerce\models\subscriptions\SwitchPlansForm;
use craft\commerce\models\Transaction;
use craft\commerce\Plugin;
use craft\commerce\Plugin as CommercePlugin;
use craft\commerce\records\Transaction as TransactionRecord;
Expand Down Expand Up @@ -484,6 +485,9 @@ public function handleWebhook(array $data): void
case 'payment_method.detached':
$this->handlePaymentMethodDetached($data);
break;
case 'payment_intent.succeeded':
$this->handlePaymentIntentSucceeded($data);
break;
case 'charge.refunded':
$this->handleRefunded($data);
break;
Expand Down Expand Up @@ -517,6 +521,47 @@ public function handleWebhook(array $data): void
parent::handleWebhook($data);
}

/**
* @param array $data
* @return void
* @throws InvalidConfigException
* @since 4.1.0
*/
public function handlePaymentIntentSucceeded(array $data): void
{
$paymentIntent = $data['data']['object'];
if ($paymentIntent['object'] === 'payment_intent') {
$transaction = Plugin::getInstance()->getTransactions()->getTransactionByReference($paymentIntent['id']);
$updateTransaction = null;

if ($transaction->parentId === null) {
$children = Plugin::getInstance()->getTransactions()->getChildrenByTransactionId($transaction->id);

if (empty($children) && $transaction->status === TransactionRecord::STATUS_PROCESSING) {
$updateTransaction = $transaction;
}

foreach ($children as $child) {
if ($child->reference === $transaction->reference && $child->status === TransactionRecord::STATUS_PROCESSING && $paymentIntent['status'] === 'succeeded') {
$updateTransaction = $child;

break;
}
}
}

if ($updateTransaction) {
$transactionRecord = TransactionRecord::findOne($updateTransaction->id);
$transactionRecord->status = TransactionRecord::STATUS_SUCCESS;
$transactionRecord->message = '';
$transactionRecord->response = $paymentIntent;

$transactionRecord->save(false);
$transaction->getOrder()->updateOrderPaidInformation();
}
}
}

/**
* Handle a failed invoice by updating the subscription data for the subscription it failed.
*
Expand Down Expand Up @@ -688,10 +733,16 @@ public function handlePaymentMethodUpdated(array $data)
$paymentSource->customerId = $user->id;
$paymentSource->response = Json::encode($stripePaymentMethod);

if ($stripePaymentMethod['card']['brand'] && $stripePaymentMethod['card']['last4']) {
$paymentSource->description = $stripePaymentMethod['card']['brand'] . ' ending in ' . $stripePaymentMethod['card']['last4'];
$description = 'Stripe payment source';

if ($stripePaymentMethod['type'] === 'card') {
$description = ($stripePaymentMethod['card']['brand'] ?: 'Card') . ' ending in ' . $stripePaymentMethod['card']['last4'];
} elseif (isset($stripePaymentMethod[$stripePaymentMethod['type']], $stripePaymentMethod[$stripePaymentMethod['type']]['last4'])) {
$description = 'Payment source ending in ' . $stripePaymentMethod[$stripePaymentMethod['type']]['last4'];
}

$paymentSource->description = $description;

$paymentMethod = $this->getStripeClient()->paymentMethods->retrieve($stripePaymentMethod['id']);
$paymentMethod->attach(['customer' => $stripeCustomer->id]);

Expand Down
52 changes: 44 additions & 8 deletions src/gateways/PaymentIntents.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use craft\commerce\base\RequestResponseInterface;
use craft\commerce\base\SubscriptionResponseInterface;
use craft\commerce\behaviors\CustomerBehavior;
use craft\commerce\elements\Order;
use craft\commerce\elements\Subscription;
use craft\commerce\errors\PaymentSourceCreatedLaterException;
use craft\commerce\errors\SubscriptionException;
Expand All @@ -34,6 +35,7 @@
use craft\commerce\stripe\web\assets\elementsform\ElementsFormAsset;
use craft\commerce\stripe\web\assets\intentsform\IntentsFormAsset;
use craft\elements\User;
use craft\helpers\ArrayHelper;
use craft\helpers\Json;
use craft\helpers\StringHelper;
use craft\helpers\UrlHelper;
Expand Down Expand Up @@ -155,27 +157,57 @@ public function getPaymentFormHtml(array $params): ?string
'submitButtonText' => Craft::t('commerce', 'Pay'),
'processingButtonText' => Craft::t('commerce', 'Processing…'),
'paymentFormType' => self::PAYMENT_FORM_TYPE_ELEMENTS,

];

$params = array_merge($defaults, $params);
/** @var ?Order $order */
$order = $params['order'] ?? null;
if ($order && $order->getBillingAddress()) {
$defaultBillingAddressValues = [
'country' => $order->getBillingAddress()->getCountryCode() ?: '',
'line1' => $order->getBillingAddress()->addressLine1 ?? '',
'line2' => $order->getBillingAddress()->addressLine2 ?? '',
'city' => $order->getBillingAddress()->locality ?? '',
'postal_code' => $order->getBillingAddress()->postalCode ?? '',
'state' => $order->getBillingAddress()->getAdministrativeArea() ?? '',
];

$defaults['elementOptions']['defaultValues'] = [
'billingDetails' => [
'name' => $order->getBillingAddress()->fullName ?? '',
'email' => $order->email,
'address' => $defaultBillingAddressValues,
],
];
}

$params = ArrayHelper::merge($defaults, $params);

if ($params['scenario'] == '') {
return Craft::t('commerce-stripe', 'Commerce Stripe 4.0+ requires a scenario is set on the payment form.');
}

$view = Craft::$app->getView();
$previousMode = $view->getTemplateMode();
$view->setTemplateMode(View::TEMPLATE_MODE_CP);
if (Craft::$app->getRequest()->isCpRequest) {
$view->setTemplateMode(View::TEMPLATE_MODE_CP);
}

$view->registerScript('', View::POS_END, ['src' => 'https://js.stripe.com/v3/']); // we need this to load at end of body

if ($params['paymentFormType'] == self::PAYMENT_FORM_TYPE_CHECKOUT) {
$html = $view->renderTemplate('commerce-stripe/paymentForms/checkoutForm', $params);
} else {
if ($params['paymentFormType'] == self::PAYMENT_FORM_TYPE_ELEMENTS) {
$view->registerAssetBundle(ElementsFormAsset::class);
$html = $view->renderTemplate('commerce-stripe/paymentForms/elementsForm', $params);
}

// Template mode needs to be CP for the payment form to work
$view->setTemplateMode(View::TEMPLATE_MODE_CP);

$templatePath = ($params['paymentFormType'] == self::PAYMENT_FORM_TYPE_CHECKOUT)
? 'commerce-stripe/paymentForms/checkoutForm'
: 'commerce-stripe/paymentForms/elementsForm';

$html = $view->renderTemplate($templatePath, $params);

$view->setTemplateMode($previousMode);

return $html;
Expand Down Expand Up @@ -216,13 +248,17 @@ public function completePurchase(Transaction $transaction): RequestResponseInter
{
$data = Json::decodeIfJson($transaction->response);

$paymentIntentOptions = [
'expand' => ['payment_method'],
];

if ($data['object'] == 'payment_intent') {
$paymentIntent = $this->getStripeClient()->paymentIntents->retrieve($data['id']);
$paymentIntent = $this->getStripeClient()->paymentIntents->retrieve($data['id'], $paymentIntentOptions);
} else {
// Likely a checkout object
$checkoutSession = $this->getStripeClient()->checkout->sessions->retrieve($data['id']);
$paymentIntent = $checkoutSession['payment_intent'];
$paymentIntent = $this->getStripeClient()->paymentIntents->retrieve($paymentIntent);
$paymentIntent = $this->getStripeClient()->paymentIntents->retrieve($paymentIntent, $paymentIntentOptions);
}

return $this->createPaymentResponseFromApiResource($paymentIntent);
Expand Down
8 changes: 8 additions & 0 deletions src/responses/PaymentIntentResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ public function isProcessing(): bool
}
}

if ((!array_key_exists('next_action', $this->data) || $this->data['next_action'] === null) && array_key_exists('status', $this->data) && $this->data['status'] === 'processing') {
return true;
}

return false;
}

Expand All @@ -67,6 +71,10 @@ public function isProcessing(): bool
*/
public function isRedirect(): bool
{
if (array_key_exists('last_payment_error', $this->data) && !empty($this->data['last_payment_error']) && (!array_key_exists('next_action', $this->data) || empty($this->data['next_action']))) {
return false;
}

if (array_key_exists('status', $this->data) && $this->data['status'] === 'requires_payment_method') {
return true;
}
Expand Down

0 comments on commit a82824f

Please sign in to comment.