Skip to content

Commit

Permalink
Merge branch 'release/4.1.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
nfourtythree committed Jan 12, 2024
2 parents a82824f + 3da7505 commit 353ef2d
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 104 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
# Release Notes for Stripe for Craft Commerce

## 4.1.1 - 2024-01-12

- Fixed a bug where legacy default payment methods were not being set as default. ([#280](https://github.com/craftcms/commerce-stripe/pull/280))
- Fixed a bug that could cause duplicate payment sources to be created. ([#281](https://github.com/craftcms/commerce-stripe/pull/281))
- Fixed a bug where it wasn’t possible to access the Stripe instance from JavaScript. ([#275](https://github.com/craftcms/commerce-stripe/issues/275))
- Fixed a bug where not all enabled payment methods types were being shown when creating a payment source. ([#251](https://github.com/craftcms/commerce-stripe/issues/251), [#160](https://github.com/craftcms/commerce-stripe/pull/160))
- Fixed a bug where changing a partial payment amount wouldn’t update the payment intent. ([#279](https://github.com/craftcms/commerce-stripe/issues/279))

## 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 it wasn’t possible to pay using the SEPA Direct Debit payment method. ([#265](https://github.com/craftcms/commerce-stripe/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()`.
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"prettier": "^2.7.1"
},
"scripts": {
"prepare": "husky install"
"prepare": "husky install",
"check-prettier": "prettier --check .",
"fix-prettier": "prettier --write ."
}
}
16 changes: 10 additions & 6 deletions src/base/Gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,7 @@ public function supportsWebhooks(): bool
}

/**
* @return string
* @throws \Exception
* @since 2.3.1
* @inheritDoc
*/
public function getTransactionHashFromWebhook(): ?string
{
Expand All @@ -464,7 +462,13 @@ public function getTransactionHashFromWebhook(): ?string

$transactionHash = ArrayHelper::getValue($data, 'data.object.metadata.transaction_reference');
if (!$transactionHash || !is_string($transactionHash)) {
return null;
$transactionHash = null;
}

if (!$transactionHash) {
// Use the object ID as the unique ID of the stripe object for the transaction hash so we can enforce a mutex
// in \craft\commerce\services\Webhooks::processWebhook() which call this method.
$transactionHash = ArrayHelper::getValue($data, 'data.object.id');
}

return $transactionHash;
Expand Down Expand Up @@ -668,10 +672,10 @@ public function createSetupIntent($params): SetupIntent
{
$defaults = [
'usage' => 'off_session',
'payment_method_types' => ['card'],
'automatic_payment_methods' => ['enabled' => true],
];

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

$event = new BuildSetupIntentRequestEvent([
'request' => $params,
Expand Down
48 changes: 30 additions & 18 deletions src/base/SubscriptionGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
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 All @@ -34,6 +33,7 @@
use craft\errors\ElementNotFoundException;
use craft\helpers\DateTimeHelper;
use craft\helpers\Json;
use craft\helpers\StringHelper;
use craft\web\View;
use Stripe\ApiResource;
use Stripe\Invoice as StripeInvoice;
Expand Down Expand Up @@ -719,38 +719,48 @@ public function handlePaymentMethodUpdated(array $data)
}

$user = Craft::$app->getUsers()->getUserById($customer->userId);

// Ensure customer actually exists in Stripe
$stripeCustomer = $this->getStripeClient()->customers->retrieve($stripePaymentMethod['customer']);

if (!$stripeCustomer) {
return;
}

// See if we have a Commerce payment source for this stripe payment method already or create one
$paymentSource = CommercePlugin::getInstance()->getPaymentSources()->getPaymentSourceByTokenAndGatewayId($stripePaymentMethod['id'], $this->id);

if (!$paymentSource) {
$paymentSource = new PaymentSource();
}

if ($stripeCustomer) {
$paymentSource->gatewayId = $this->id;
$paymentSource->token = $stripePaymentMethod['id'];
$paymentSource->customerId = $user->id;
$paymentSource->response = Json::encode($stripePaymentMethod);
$paymentSource->gatewayId = $this->id;
$paymentSource->token = $stripePaymentMethod['id'];
$paymentSource->customerId = $user->id;
$paymentSource->response = Json::encode($stripePaymentMethod);

if (!$paymentSource->id || !$paymentSource->description) {
$description = 'Stripe payment source';

if ($stripePaymentMethod['type'] === 'card') {
$description = ($stripePaymentMethod['card']['brand'] ?: 'Card') . ' ending in ' . $stripePaymentMethod['card']['last4'];
$last4 = $stripePaymentMethod['card']['last4'];
$brand = $stripePaymentMethod['card']['brand'] ?: 'Card';
$description = Craft::t('commerce-stripe', '{cardType} ending in ••••{last4}', ['cardType' => StringHelper::upperCaseFirst($brand), 'last4' => $last4]);
} elseif (isset($stripePaymentMethod[$stripePaymentMethod['type']], $stripePaymentMethod[$stripePaymentMethod['type']]['last4'])) {
$description = 'Payment source ending in ' . $stripePaymentMethod[$stripePaymentMethod['type']]['last4'];
$last4 = $stripePaymentMethod[$stripePaymentMethod['type']]['last4'];
$description = Craft::t('commerce-stripe', 'Payment method ending in ••••{last4}', ['last4' => $last4]);
}

$paymentSource->description = $description;
}

$paymentMethod = $this->getStripeClient()->paymentMethods->retrieve($stripePaymentMethod['id']);
$paymentMethod->attach(['customer' => $stripeCustomer->id]);
// No harm in making sure it is attached to the customer.
$this->getStripeClient()->paymentMethods->attach($stripePaymentMethod['id'], ['customer' => $stripeCustomer->id]);

$result = Plugin::getInstance()->paymentSources->savePaymentSource($paymentSource);
$result = Plugin::getInstance()->paymentSources->savePaymentSource($paymentSource);

if (!$result) {
Craft::error('Could not save payment source: ' . Json::encode($paymentSource->getErrors()), 'commerce-stripe');
}
if (!$result) {
Craft::error('Could not save payment source: ' . Json::encode($paymentSource->getErrors()), 'commerce-stripe');
}
}
}
Expand Down Expand Up @@ -812,10 +822,12 @@ public function handleCustomerUpdated(array $data): void
{
$stripeCustomer = $data['data']['object'];

// Set the primary payment source for the user if it has changed
if (isset($stripeCustomer['invoice_settings']['default_payment_method'])) {
$paymentMethodId = $stripeCustomer['invoice_settings']['default_payment_method'];
$defaultPaymentMethod = $stripeCustomer['invoice_settings']['default_payment_method']
?? $stripeCustomer['default_source']
?? null;

// Set the primary payment source for the user if it has changed
if ($defaultPaymentMethod) {
$customer = StripePlugin::getInstance()->getCustomers()->getCustomerByReference($stripeCustomer['id'], $this->id);
if (!$customer) {
return;
Expand All @@ -827,7 +839,7 @@ public function handleCustomerUpdated(array $data): void
return;
}

$paymentSource = CommercePlugin::getInstance()->getPaymentSources()->getPaymentSourceByTokenAndGatewayId($paymentMethodId, $this->id);
$paymentSource = CommercePlugin::getInstance()->getPaymentSources()->getPaymentSourceByTokenAndGatewayId($defaultPaymentMethod, $this->id);
if (!$paymentSource) {
return;
}
Expand Down
1 change: 0 additions & 1 deletion src/controllers/CustomersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ public function actionCreateSetupIntent(): Response
$gateway = CommercePlugin::getInstance()->getGateways()->getGatewayById((int)$gatewayId);
$setupIntent = [
'customer' => $customer->reference,
'payment_method_types' => ['bancontact', 'card', 'ideal'],
];
return $this->asJson($gateway->createSetupIntent($setupIntent));
} catch (Throwable $e) {
Expand Down
27 changes: 21 additions & 6 deletions src/gateways/PaymentIntents.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
use Stripe\PaymentIntent;
use Throwable;
use yii\base\NotSupportedException;
use function count;

/**
* This class represents the Stripe Payment Intents gateway
Expand Down Expand Up @@ -97,7 +96,7 @@ public function getOldPaymentFormHtml(array $params): ?string
'handle' => $this->handle,
];

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

// If there's no order passed, add the current cart if we're not messing around in backend.
if (!isset($params['order']) && !Craft::$app->getRequest()->getIsCpRequest()) {
Expand Down Expand Up @@ -152,12 +151,12 @@ public function getPaymentFormHtml(array $params): ?string
'type' => 'tabs',
],
],
'hiddenClass' => 'hidden',
'submitButtonClasses' => '',
'errorMessageClasses' => '',
'submitButtonText' => Craft::t('commerce', 'Pay'),
'processingButtonText' => Craft::t('commerce', 'Processing…'),
'paymentFormType' => self::PAYMENT_FORM_TYPE_ELEMENTS,

];

/** @var ?Order $order */
Expand Down Expand Up @@ -331,6 +330,12 @@ public function createPaymentSource(BasePaymentForm $sourceData, int $customerId

/** @var PaymentIntentForm $sourceData */
try {
$lockName = "commerceTransaction:{$sourceData->paymentMethodId}";

if (!Craft::$app->getMutex()->acquire($lockName, 15)) {
throw new Exception("Unable to acquire mutex lock: $lockName");
}

$stripeCustomer = $this->getStripeCustomer($customerId);
$paymentMethod = $this->getStripeClient()->paymentMethods->retrieve($sourceData->paymentMethodId);
$paymentMethod = $paymentMethod->attach(['customer' => $stripeCustomer->id]);
Expand All @@ -342,7 +347,11 @@ public function createPaymentSource(BasePaymentForm $sourceData, int $customerId
$description = Craft::t('commerce-stripe', '{cardType} ending in ••••{last4}', ['cardType' => StringHelper::upperCaseFirst($card->brand), 'last4' => $card->last4]);
break;
default:
$description = $paymentMethod->type;
if (isset($paymentMethod->{$paymentMethod->type}, $paymentMethod->{$paymentMethod->type}->last4)) {
$description = Craft::t('commerce-stripe', 'Payment method ending in ••••{last4}', ['last4' => $paymentMethod->{$paymentMethod->type}->last4]);
} else {
$description = $paymentMethod->type;
}
}

// Make it the default in Stripe if its the only one for this gateway
Expand All @@ -363,9 +372,11 @@ public function createPaymentSource(BasePaymentForm $sourceData, int $customerId
$paymentSource->response = $paymentMethod->toJSON() ?? '';
$paymentSource->description = $description;

Craft::$app->getMutex()->release($lockName);

return $paymentSource;
} catch (Throwable $exception) {
Craft::$app->getMutex()->release($lockName);
throw new PaymentSourceException($exception->getMessage());
}
}
Expand All @@ -378,9 +389,13 @@ public function subscribe(User $user, BasePlan $plan, BaseSubscriptionForm $para
{
/** @var SubscriptionForm $parameters */
$customer = StripePlugin::getInstance()->getCustomers()->getCustomer($this->id, $user);
$paymentMethods = $this->getStripeClient()->paymentMethods->all(['customer' => $customer->reference, 'type' => 'card']);
$stripeCustomer = $this->getStripeClient()->customers->retrieve($customer->reference);

$defaultPaymentMethod = $stripeCustomer['invoice_settings']['default_payment_method']
?? $stripeCustomer['default_source'] // backward compatible
?? null;

if (count($paymentMethods->data) === 0) {
if (!$defaultPaymentMethod) {
throw new PaymentSourceException(Craft::t('commerce-stripe', 'No payment sources are saved to use for subscriptions.'));
}

Expand Down
14 changes: 12 additions & 2 deletions src/services/PaymentMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@

namespace craft\commerce\stripe\services;

use Craft;
use craft\commerce\events\UpdatePrimaryPaymentSourceEvent;
use craft\commerce\Plugin as CommercePlugin;
use craft\commerce\stripe\base\Gateway;
use craft\commerce\stripe\base\SubscriptionGateway;
use craft\commerce\stripe\Plugin;
use Exception;

/**
* Payment sources service.
Expand All @@ -36,12 +38,20 @@ public function syncAllPaymentMethods(Gateway $gateway)

foreach ($customers->autoPagingIterator() as $customer) {
$stripePaymentMethods = $customer->allPaymentMethods(
$customer->id,
['type' => 'card'],
$customer->id
);

foreach ($stripePaymentMethods as $stripePaymentMethod) {
$lockName = "commerceTransaction:{$stripePaymentMethod['id']}";

if (!Craft::$app->getMutex()->acquire($lockName, 15)) {
throw new Exception("Unable to acquire mutex lock: $lockName");
}

$gateway->handlePaymentMethodUpdated($stripePaymentMethod->toArray());

Craft::$app->getMutex()->release($lockName);

$count++;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/templates/paymentForms/elementsForm.twig
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
data-client-secret="{{ clientSecret }}"
data-client-scenario="{{ scenario }}"
data-processing-button-text="{{ processingButtonText }}"
data-hidden-class="{{ hiddenClass }}"
>

<div class="hidden stripe-error-message {{ errorMessageClasses }}">
{# Error messages are displayed to your customers, here. #}
</div>
Expand Down
Loading

0 comments on commit 353ef2d

Please sign in to comment.