Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[12.x] Stripe Checkout Support #1007

Merged
merged 11 commits into from
Jan 27, 2021
30 changes: 30 additions & 0 deletions resources/views/checkout.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<button
id="checkout-{{ $sessionId }}"
role="link"
style="{{ isset($style) && ! isset($class) ? $style : 'background-color:#6772E5;color:#FFF;padding:8px 12px;border:0;border-radius:4px;font-size:1em' }}"
@isset($class) class="{{ $class }}" @endisset
>
{{ $label }}
</button>

<div id="error-message"></div>

<script>
(() => {
const checkoutButton = document.getElementById('checkout-{{ $sessionId }}');

checkoutButton.addEventListener('click', function () {
// When the customer clicks on the button, redirect them to Checkout.
Stripe('{{ $stripeKey }}').redirectToCheckout({
sessionId: '{{ $sessionId }}'
}).then(function (result) {
// If `redirectToCheckout` fails due to a browser or network
// error, display the localized error message to your customer
// using `result.error.message`.
if (result.error) {
document.getElementById('error-message').innerText = result.error.message;
}
});
});
})()
</script>
96 changes: 96 additions & 0 deletions src/Checkout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace Laravel\Cashier;

use Illuminate\Support\Facades\View;
use Stripe\Checkout\Session;

class Checkout
{
/**
* The Stripe model instance.
*
* @var \Illuminate\Database\Eloquent\Model
*/
protected $owner;

/**
* The Stripe Checkout Session instance.
*
* @var \Stripe\Checkout\Session
*/
protected $session;

/**
* Create a new Checkout Session instance.
*
* @param \Illuminate\Database\Eloquent\Model $owner
* @param \Stripe\Checkout\Session $session
* @return void
*/
public function __construct($owner, Session $session)
{
$this->owner = $owner;
$this->session = $session;
}

/**
* Begin a new Checkout Session.
*
* @param \Illuminate\Database\Eloquent\Model $owner
* @param array $sessionOptions
* @param array $customerOptions
* @return \Laravel\Cashier\Checkout
*/
public static function create($owner, array $sessionOptions = [], array $customerOptions = [])
{
$customer = $owner->createOrGetStripeCustomer($customerOptions);

$session = Session::create(array_merge([
'customer' => $customer->id,
'mode' => 'payment',
'success_url' => $sessionOptions['success_url'] ?? route('home').'?checkout=success',
'cancel_url' => $sessionOptions['cancel_url'] ?? route('home').'?checkout=cancelled',
'payment_method_types' => ['card'],
], $sessionOptions), Cashier::stripeOptions());

return new static($customer, $session);
}

/**
* Get the View instance for the button.
*
* @param string $label
* @param array $options
* @return \Illuminate\Contracts\View\View
*/
public function button($label = 'Check out', array $options = [])
{
return View::make('cashier::checkout', array_merge([
'label' => $label,
'sessionId' => $this->session->id,
'stripeKey' => config('cashier.key'),
], $options));
}

/**
* Get the Checkout Session as a Stripe Checkout Session object.
*
* @return \Stripe\Checkout\Session
*/
public function asStripeCheckoutSession()
{
return $this->session;
}

/**
* Dynamically get values from the Stripe Checkout Session.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->session->{$key};
}
}
76 changes: 76 additions & 0 deletions src/Concerns/PerformsCharges.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

namespace Laravel\Cashier\Concerns;

use Laravel\Cashier\Checkout;
use Laravel\Cashier\Payment;
use Stripe\PaymentIntent as StripePaymentIntent;
use Stripe\Refund as StripeRefund;

trait PerformsCharges
{
/**
* Determines if user redeemable promotion codes are available in Stripe Checkout.
*
* @var bool
*/
protected $allowPromotionCodes = false;

/**
* Make a "one off" charge on the customer for the given amount.
*
Expand Down Expand Up @@ -57,4 +65,72 @@ public function refund($paymentIntent, array $options = [])
$this->stripeOptions()
);
}

/**
* Begin a new Checkout Session for existing Prices.
*
* @param array|string $items
* @param int $quantity
* @param array $sessionOptions
* @param array $customerOptions
* @return \Laravel\Cashier\Checkout
*/
public function checkout($items, array $sessionOptions = [], array $customerOptions = [])
{
$items = collect((array) $items)->map(function ($item, $key) {
// If the key is a string, we'll assume it's a Price ID and its value is its quantity.
if (is_string($key)) {
return ['price' => $key, 'quantity' => $item];
}

// If the value is a string, we'll assume it's a Price ID.
$item = is_string($item) ? ['price' => $item] : $item;

// Ensure a quantity is set.
$item['quantity'] = $item['quantity'] ?? 1;

return $item;
})->values()->all();

return Checkout::create($this, array_merge([
'allow_promotion_codes' => $this->allowPromotionCodes,
'line_items' => $items,
], $sessionOptions), $customerOptions);
}

/**
* Begin a new Checkout Session for a "one-off" charge.
*
* @param int $amount
* @param string $name
* @param int $quantity
* @param array $sessionOptions
* @param array $customerOptions
* @return \Laravel\Cashier\Checkout
*/
public function checkoutCharge($amount, $name, $quantity = 1, array $sessionOptions = [], array $customerOptions = [])
{
return $this->checkout([[
'price_data' => [
'currency' => $this->preferredCurrency(),
'product_data' => [
'name' => $name,
],
'unit_amount' => $amount,
],
'quantity' => $quantity,
]], $sessionOptions, $customerOptions);
}

/**
* Enables user redeemable promotion codes.
*
* @return $this
*/
public function allowPromotionCodes()
{
$this->allowPromotionCodes = true;

return $this;
}
}
43 changes: 43 additions & 0 deletions src/Http/Controllers/WebhookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,49 @@ public function handleWebhook(Request $request)
return $this->missingMethod();
}

/**
* Handle customer subscription created.
*
* @param array $payload
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function handleCustomerSubscriptionCreated(array $payload)
{
$user = $this->getUserByStripeId($payload['data']['object']['customer']);

if ($user) {
$data = $payload['data']['object'];

if (! $user->subscriptions->contains('stripe_id', $data['id'])) {
if (isset($data['trial_end'])) {
$trialEndsAt = Carbon::createFromTimestamp($data['trial_end']);
} else {
$trialEndsAt = null;
}

$subscription = $user->subscriptions()->create([
'name' => $data['metadata']['name'],
'stripe_id' => $data['id'],
'stripe_status' => $data['status'],
'stripe_plan' => $data['plan']['id'] ?? null,
'quantity' => $data['quantity'],
'trial_ends_at' => $trialEndsAt,
'ends_at' => null,
]);

foreach ($data['items']['data'] as $item) {
$subscription->items()->create([
'stripe_id' => $item['id'],
'stripe_plan' => $item['plan']['id'],
'quantity' => $item['quantity'],
]);
}
}
}

return $this->successMethod();
}

/**
* Handle customer subscription updated.
*
Expand Down
6 changes: 4 additions & 2 deletions src/Subscription.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ class Subscription extends Model
* @var array
*/
protected $dates = [
'trial_ends_at', 'ends_at',
'created_at', 'updated_at',
'created_at',
'ends_at',
'trial_ends_at',
'updated_at',
];

/**
Expand Down
64 changes: 59 additions & 5 deletions src/SubscriptionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,19 @@ class SubscriptionBuilder
*/
protected $promotionCode;

/**
* Determines if user redeemable promotion codes are available in Stripe Checkout.
*
* @var bool
*/
protected $allowPromotionCodes = false;

/**
* The metadata to apply to the subscription.
*
* @var array|null
* @var array
*/
protected $metadata;
protected $metadata = [];

/**
* Create a new subscription builder instance.
Expand Down Expand Up @@ -106,7 +113,7 @@ public function __construct($owner, $name, $plans = null)
public function plan($plan, $quantity = 1)
{
$options = [
'plan' => $plan,
'price' => $plan,
'quantity' => $quantity,
];

Expand All @@ -133,7 +140,7 @@ public function quantity($quantity, $plan = null)
throw new InvalidArgumentException('Plan is required when creating multi-plan subscriptions.');
}

$plan = Arr::first($this->items)['plan'];
$plan = Arr::first($this->items)['price'];
}

return $this->plan($plan, $quantity);
Expand Down Expand Up @@ -220,6 +227,18 @@ public function withPromotionCode($promotionCode)
return $this;
}

/**
* Enables user redeemable promotion codes.
*
* @return $this
*/
public function allowPromotionCodes()
{
$this->allowPromotionCodes = true;

return $this;
}

/**
* The metadata to apply to a new subscription.
*
Expand All @@ -228,7 +247,7 @@ public function withPromotionCode($promotionCode)
*/
public function withMetadata($metadata)
{
$this->metadata = $metadata;
$this->metadata = (array) $metadata;

return $this;
}
Expand Down Expand Up @@ -309,6 +328,41 @@ public function create($paymentMethod = null, array $customerOptions = [], array
return $subscription;
}

/**
* Begin a new Checkout Session.
*
* @param array $sessionOptions
* @param array $customerOptions
* @return \Laravel\Cashier\Checkout
*/
public function checkout(array $sessionOptions = [], array $customerOptions = [])
{
if (! $this->skipTrial && $this->trialExpires) {
// Checkout Sessions are active for 24 hours after their creation and within that time frame the customer
// can complete the payment at any time. Stripe requires the trial end at least 48 hours in the future
// so that there is still at least a one day trial if your customer pays at the end of the 24 hours.
$minimumTrialPeriod = Carbon::now()->addHours(48);

$trialEnd = $this->trialExpires->gt($minimumTrialPeriod) ? $this->trialExpires : $minimumTrialPeriod;
} else {
$trialEnd = null;
}

return Checkout::create($this->owner, array_merge([
'mode' => 'subscription',
'line_items' => collect($this->items)->values()->all(),
'allow_promotion_codes' => $this->allowPromotionCodes,
'discounts' => [
'coupon' => $this->coupon,
],
'default_tax_rates' => $this->getTaxRatesForPayload(),
'subscription_data' => [
'trial_end' => $trialEnd ? $trialEnd->getTimestamp() : null,
'metadata' => array_merge($this->metadata, ['name' => $this->name]),
],
], $sessionOptions), $customerOptions);
}

/**
* Get the Stripe customer instance for the current user and payment method.
*
Expand Down
Loading