Skip to content

Commit

Permalink
Merge pull request #667 from laravel/billing-sca
Browse files Browse the repository at this point in the history
[10.0] Payment Intents
  • Loading branch information
taylorotwell authored Jul 5, 2019
2 parents c30353d + e032594 commit 80d5461
Show file tree
Hide file tree
Showing 24 changed files with 890 additions and 77 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"illuminate/contracts": "~5.8.0|~5.9.0",
"illuminate/database": "~5.8.0|~5.9.0",
"illuminate/http": "~5.8.0|~5.9.0",
"illuminate/mail": "~5.8.0|~5.9.0",
"illuminate/routing": "~5.8.0|~5.9.0",
"illuminate/support": "~5.8.0|~5.9.0",
"illuminate/view": "~5.8.0|~5.9.0",
Expand Down
26 changes: 26 additions & 0 deletions config/cashier.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@

'secret' => env('STRIPE_SECRET'),

/*
|--------------------------------------------------------------------------
| Cashier Path
|--------------------------------------------------------------------------
|
| This is the base URI path where Cashier's views, such as the payment
| verification screen, will be available from. You're free to tweak
| this path according to your preferences and application design.
|
*/

'path' => env('CASHIER_PATH', 'stripe'),

/*
|--------------------------------------------------------------------------
| Stripe Webhooks
Expand Down Expand Up @@ -72,4 +85,17 @@

'currency_locale' => env('CASHIER_CURRENCY_LOCALE', 'en'),

/*
|--------------------------------------------------------------------------
| Payment Confirmation Emails
|--------------------------------------------------------------------------
|
| When this option is enabled, Cashier will automatically email customers
| whose payments require additional verification. You should listen to
| Stripe's webhooks in order for this feature to function correctly.
|
*/

'payment_emails' => env('CASHIER_PAYMENT_EMAILS', false),

];
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function up()
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->string('name');
$table->string('status');
$table->string('stripe_id')->collation('utf8mb4_bin');
$table->string('stripe_plan');
$table->integer('quantity');
Expand Down
19 changes: 19 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"All rights reserved.": "All rights reserved.",
"Card": "Card",
"Confirm Payment": "Confirm Payment",
"Confirm your :amount payment": "Confirm your :amount payment",
"Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.": "Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.",
"Extra confirmation is needed to process your payment. Please continue to the payment page by clicking on the button below.": "Extra confirmation is needed to process your payment. Please continue to the payment page by clicking on the button below.",
"Full name": "Full name",
"Go back": "Go back",
"Pay :amount": "Pay :amount",
"Payment Cancelled": "Payment Cancelled",
"Payment Confirmation": "Payment Confirmation",
"Payment Successful": "Payment Successful",
"Please provide your name.": "Please provide your name.",
"Thanks": "Thanks",
"The payment was successful.": "The payment was successful.",
"This payment was already successfully confirmed.": "This payment was already successfully confirmed.",
"This payment was cancelled.": "This payment was cancelled."
}
12 changes: 12 additions & 0 deletions resources/views/emails/confirm_payment.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@component('mail::message')
# {{ __('Confirm your :amount payment', ['amount' => $payment->amount()]) }}

{{ __('Extra confirmation is needed to process your payment. Please continue to the payment page by clicking on the button below.') }}

@component('mail::button', ['url' => route('cashier.payment', ['id' => $payment->id()])])
{{ __('Confirm Payment') }}
@endcomponent

{{ __('Thanks') }},<br>
{{ config('app.name') }}
@endcomponent
108 changes: 108 additions & 0 deletions resources/views/payment.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />

<title>{{ __('Payment Confirmation') }} - {{ config('app.name', 'Laravel') }}</title>

<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">

<script src="https://js.stripe.com/v3"></script>
</head>
<body class="font-sans text-gray-600 bg-gray-200 leading-normal p-4 h-full">
<div class="h-full md:flex md:justify-center md:items-center">
<div class="w-full max-w-lg">
<p id="message" class="hidden mb-4 bg-red-100 border border-red-400 px-6 py-4 rounded-lg text-red-600"></p>

<div class="bg-white rounded-lg shadow-xl p-4 sm:py-6 sm:px-10 mb-5">
@if ($payment->isSucceeded())
<h1 class="text-xl mt-2 mb-4 text-gray-700">
{{ __('Payment Successful') }}
</h1>

<p class="mb-6">{{ __('This payment was already successfully confirmed.') }}</p>
@elseif ($payment->isCancelled())
<h1 class="text-xl mt-2 mb-4 text-gray-700">
{{ __('Payment Cancelled') }}
</h1>

<p class="mb-6">{{ __('This payment was cancelled.') }}</p>
@else
<div id="payment-elements">
<h1 class="text-xl mt-2 mb-4 text-gray-700">
{{ __('Confirm your :amount payment', ['amount' => $payment->amount()]) }}
</h1>

<p class="mb-6">
{{ __('Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.') }}
</p>

<label for="cardholder-name" class="inline-block text-sm text-gray-700 font-semibold mb-2">{{ __('Full name') }}</label>
<input id="cardholder-name" type="text" placeholder="Jane Doe" required
class="inline-block bg-gray-200 border border-gray-400 rounded-lg w-full px-4 py-3 mb-3">

<label for="cardholder-name" class="inline-block text-sm text-gray-700 font-semibold mb-2">{{ __('Card') }}</label>
<div id="card-element" class="bg-gray-200 border border-gray-400 rounded-lg p-4 mb-6"></div>

<button id="card-button" class="inline-block w-full px-4 py-3 mb-4 bg-blue-600 text-white rounded-lg hover:bg-blue-500">
{{ __('Pay :amount', ['amount' => $payment->amount()]) }}
</button>
</div>
@endif

<a href="{{ $redirect ?? url('/') }}"
class="inline-block w-full px-4 py-3 bg-gray-200 hover:bg-gray-300 text-center text-gray-700 rounded-lg">
{{ __('Go back') }}
</a>

<script>
const paymentElements = document.getElementById('payment-elements');
const cardholderName = document.getElementById('cardholder-name');
const cardButton = document.getElementById('card-button');
const message = document.getElementById('message');
const stripe = Stripe('{{ $stripeKey }}');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
cardButton.addEventListener('click', function() {
stripe.handleCardPayment(
'{{ $payment->clientSecret() }}', cardElement, {
payment_method_data: {
billing_details: { name: cardholderName.value }
}
}
).then(function (result) {
if (result.error) {
if (result.error.code === 'parameter_invalid_empty' &&
result.error.param === 'payment_method_data[billing_details][name]') {
message.innerText = '⚠️ {{ __('Please provide your name.') }}';
} else {
message.innerText = '⚠️ '+result.error.message;
}
message.classList.add('text-red-600', 'border-red-400', 'bg-red-100');
message.classList.remove('text-green-600', 'border-green-400', 'bg-green-100');
} else {
paymentElements.classList.add('hidden');
message.innerText = '{{ __('The payment was successful.') }}';
message.classList.remove('text-red-600', 'border-red-400', 'bg-red-100');
message.classList.add('text-green-600', 'border-green-400', 'bg-green-100');
}
message.classList.remove('hidden');
});
});
</script>
</div>

<p class="text-center text-gray-500 text-sm">
© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }}
</p>
</div>
</div>
</body>
</html>
1 change: 1 addition & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

use Illuminate\Support\Facades\Route;

Route::get('payment/{id}', 'PaymentController@show')->name('payment');
Route::post('webhook', 'WebhookController@handleWebhook')->name('webhook');
39 changes: 28 additions & 11 deletions src/Billable.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
use Stripe\Card as StripeCard;
use Stripe\Token as StripeToken;
use Illuminate\Support\Collection;
use Stripe\Charge as StripeCharge;
use Stripe\Refund as StripeRefund;
use Stripe\Invoice as StripeInvoice;
use Stripe\Customer as StripeCustomer;
use Stripe\BankAccount as StripeBankAccount;
use Stripe\InvoiceItem as StripeInvoiceItem;
use Stripe\Error\Card as StripeCardException;
use Stripe\PaymentIntent as StripePaymentIntent;
use Laravel\Cashier\Exceptions\InvalidStripeCustomer;
use Stripe\Error\InvalidRequest as StripeErrorInvalidRequest;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
Expand All @@ -25,12 +25,14 @@ trait Billable
*
* @param int $amount
* @param array $options
* @return \Stripe\Charge
* @return \Laravel\Cashier\Payment
* @throws \InvalidArgumentException
*/
public function charge($amount, array $options = [])
{
$options = array_merge([
'confirmation_method' => 'automatic',
'confirm' => true,
'currency' => $this->preferredCurrency(),
], $options);

Expand All @@ -40,26 +42,32 @@ public function charge($amount, array $options = [])
$options['customer'] = $this->stripe_id;
}

if (! array_key_exists('source', $options) && ! array_key_exists('customer', $options)) {
throw new InvalidArgumentException('No payment source provided.');
if (! array_key_exists('payment_method', $options) && ! array_key_exists('customer', $options)) {
throw new InvalidArgumentException('No payment method provided.');
}

return StripeCharge::create($options, Cashier::stripeOptions());
$payment = new Payment(
StripePaymentIntent::create($options, Cashier::stripeOptions())
);

$payment->validate();

return $payment;
}

/**
* Refund a customer for a charge.
*
* @param string $charge
* @param string $paymentIntent
* @param array $options
* @return \Stripe\Refund
* @throws \InvalidArgumentException
*/
public function refund($charge, array $options = [])
public function refund($paymentIntent, array $options = [])
{
$options['charge'] = $charge;
$intent = StripePaymentIntent::retrieve($paymentIntent, Cashier::stripeOptions());

return StripeRefund::create($options, Cashier::stripeOptions());
return $intent->charges->data[0]->refund($options);
}

/**
Expand Down Expand Up @@ -217,9 +225,18 @@ public function invoice(array $options = [])
$parameters = array_merge($options, ['customer' => $this->stripe_id]);

try {
return StripeInvoice::create($parameters, Cashier::stripeOptions())->pay();
/** @var \Stripe\Invoice $invoice */
$invoice = StripeInvoice::create($parameters, Cashier::stripeOptions());

return $invoice->pay();
} catch (StripeErrorInvalidRequest $e) {
return false;
} catch (StripeCardException $exception) {
$payment = new Payment(
StripePaymentIntent::retrieve($invoice->refresh()->payment_intent, Cashier::stripeOptions())
);

$payment->validate();
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/CashierServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ protected function configure()
protected function registerRoutes()
{
Route::group([
'prefix' => 'stripe',
'prefix' => config('cashier.path'),
'namespace' => 'Laravel\Cashier\Http\Controllers',
'as' => 'cashier.',
], function () {
Expand All @@ -65,6 +65,7 @@ protected function registerRoutes()
*/
protected function registerResources()
{
$this->loadJsonTranslationsFrom(__DIR__.'/../resources/lang');
$this->loadViewsFrom(__DIR__.'/../resources/views', 'cashier');
}

Expand Down
33 changes: 33 additions & 0 deletions src/Exceptions/IncompletePayment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Laravel\Cashier\Exceptions;

use Exception;
use Throwable;
use Laravel\Cashier\Payment;

class IncompletePayment extends Exception
{
/**
* The Cashier Payment object.
*
* @var \Laravel\Cashier\Payment
*/
public $payment;

/**
* Create a new IncompletePayment instance.
*
* @param \Laravel\Cashier\Payment $payment
* @param string $message
* @param int $code
* @param \Throwable|null $previous
* @return void
*/
public function __construct(Payment $payment, $message = '', $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);

$this->payment = $payment;
}
}
22 changes: 22 additions & 0 deletions src/Exceptions/PaymentActionRequired.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Laravel\Cashier\Exceptions;

use Laravel\Cashier\Payment;

class PaymentActionRequired extends IncompletePayment
{
/**
* Create a new PaymentActionRequired instance.
*
* @param \Laravel\Cashier\Payment $payment
* @return self
*/
public static function incomplete(Payment $payment)
{
return new self(
$payment,
'The payment attempt failed because additional action is required before it can be completed.'
);
}
}
22 changes: 22 additions & 0 deletions src/Exceptions/PaymentFailure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Laravel\Cashier\Exceptions;

use Laravel\Cashier\Payment;

class PaymentFailure extends IncompletePayment
{
/**
* Create a new PaymentFailure instance.
*
* @param \Laravel\Cashier\Payment $payment
* @return self
*/
public static function cardError(Payment $payment)
{
return new self(
$payment,
'The payment attempt failed because of a card error.'
);
}
}
Loading

0 comments on commit 80d5461

Please sign in to comment.