Using subscriptions in Spike

Opcodes\Spike\SubscriptionPlan is the Spike's plan representation object. It contains all the necessary information about a particular subscription plan, including its name, cost, and configuration values.

When interacting with Spike's subscription methods, you'll be receiving and providing instances of SubscriptionPlan:

class SubscriptionPlan
{
    const PERIOD_MONTHLY = 'monthly';
    const PERIOD_YEARLY = 'yearly';

    public function __construct(
        public string $id,
        public string $name,
        public string $period = self::PERIOD_MONTHLY,
        public ?string $short_description = null,
        public ?array $features = [],
        public ?string $payment_provider_price_id = null,
        public ?int $price_in_cents = null,
        public array $provides_monthly = [],
        public array $options = [],
    ) {}

    public static function fromArray(array $config, $yearly = false): SubscriptionPlan;
}

The SubscriptionPlan instance has these methods available for convenience:

class SubscriptionPlan
{
    /**
     * Returns the default, free subscription plan.
     * This is used when there's no free plan configured in config/spike.php
     */
    public static function defaultFreePlan($yearly = false): static;
    
    /**
     * Returns the formatted price of this plan, such as "€10,00"
     */
    public function priceFormatted(): string;
    
    /** Check whether the plan is monthly */
    public function isMonthly(): bool;
    
    /** Check whether the plan is yearly */
    public function isYearly(): bool;
    
    /** Check whether the plan is free */
    public function isFree(): bool;
    
    /** Check whether the plan is paid */
    public function isPaid(): bool;
    
    /** Check whether the plan is the current plan of the resolved billable */
    public function isCurrent(): bool;
    
    /** Check whether the plan has been cancelled for the resolved billable */
    public function isCancelled(): bool;
}

Subscription plans

Getting a list of subscription plans

use Opcodes\Spike\Facades\Spike;

$plans = Spike::subscriptionPlans();

If you're using custom subscription plan resolvers, you can also get the subscription plans available to a specific billable instance, like so:

use App\Models\Team;
use Opcodes\Spike\Facades\Spike;

$teamPlans = Spike::subscriptionPlans($team);

Finding a subscription plan

Spike::findSubscriptionPlan(string $id) returns an instance of SubscriptionPlan for the given id or payment_provider_price_id.

If a plan is not found, it returns null.

If you have configured a custom subscription plan resolver, you can provide a specific billable instance to find the available plan for.

use Opcodes\Spike\Facades\Spike;

// find the plan with ID of 'standard'
$plan = Spike::findSubscriptionPlan('standard');

// find the plan with ID of 'standard' for the given billable instance
$plan = Spike::findSubscriptionPlan('standard', $team);

Other notable methods

Here are a few other, notable, available methods related to subscriptions.

$team = App\Models\Team::first();

// returns a boolean whether there are any subscription plans available
Spike::subscriptionPlansAvailable();
Spike::subscriptionPlansAvailable($team);

// returns a collection of monthly subscription plans
Spike::monthlySubscriptionPlans();
Spike::monthlySubscriptionPlans($team);

// returns a collection of yearly subscription plans
Spike::yearlySubscriptionPlans();
Spike::yearlySubscriptionPlans($team);

// returns the current subscription plan for the billable
Spike::currentSubscriptionPlan();
Spike::currentSubscriptionPlan($team);

// provide a custom resolver for subscription plans
Spike::resolveSubscriptionPlansUsing(callable $callback);
Spike::resolveSubscriptionPlansUsing(callable $callback);

Working with subscription collections

The above methods return a simple \Illuminate\Support\Collection instance, which allows you to utilise the helper methods available in Laravel Collections.

For example, you can fetch the cheapest yearly plan like this:

$cheapestYearlyPlan = Spike::subscriptionPlans()
    ->filter(fn ($plan) => $plan->isYearly())
    ->sortByDesc('price_in_cents')
    ->first();

Subscription actions

Subscriptions are already handled automatically by Spike via the user interface provided.

Only use these methods if you wish to handle certain subscription actions yourself.

The SpikeBillable trait you have added to the billable model provides a number of convenience methods for interacting with subscriptions. Let's look at them below.

Subscribing to a plan

$user->subscribeTo(SubscriptionPlan $plan, bool $requirePaymentCard = true) allows you to subscribe a user to a given plan. This requires the user to first have a payment method attached already. Currently, that can only be done via the user interface by the user themselves.

$plan = Spike::findSubscriptionPlan('standard');

// subscribe the user to the provided SubscriptionPlan instance.
// requires a payment method saved on the user.
$subscription = $user->subscribeTo($plan);

If you would like to subscribe without actually creating and charging for the subscription in Stripe/Paddle (essentially giving the subscription for free), you can instead call this:

$plan = Spike::findSubscriptionPlan('standard');

// subscribe the user to the provided SubscriptionPlan instance.
// does not require a payment method.
$subscription = $user->subscribeWithoutPaymentTo($plan);

Switching to a different subscription plan

$user->subscribeTo(SubscriptionPlan $plan), as described above, can also be used to switch to a different subscription plan or period (e.g. upgrading to yearly subscription). It automatically handles the user's existing subscription and any prorating if necessary.

$user->subscribeWithoutPaymentTo(SubscriptionPlan $plan) also works for switching to a different plan without actually requiring a payment method active. This will not create a subscription in Stripe/Paddle, essentially giving the user this plan for free.

Cancelling subscription

$user->cancelSubscription() allows you to cancel a user's subscription. It will go into "grace period" until the end of the billing cycle, allowing the user to continue using your product as much as they have paid for. At the end of the billing cycle, the subscription will not be renewed and any unused credits provided by that subscription will expire.

$user->cancelSubscription();

$user->cancelSubscriptionNow() allows you to cancel a user's subscription immediately. This will prorate the existing subscription, refund the user for any unused time, and remove credits prorated to that refund.

$user->cancelSubscriptionNow();

Resuming subscription

$user->resumeSubscription() allows you to resume any subscription which is currently on the "grace period". Doing this will continue automatic charges and credit renewals at the end of the billing period.

As with other methods, you can provide a custom billable instance.

$user->resumeSubscription();

Checking if subscribed

$user->isSubscribed() will return true if the user has an active subscription.

If the user has cancelled their subscription and is currently on the "grace period", this will return true until the end of their billing period.

// is the user subscribed?
$user->isSubscribed();

// is the user subscribed to this specific plan?
$user->isSubscribed($plan);

// alternative method to the above
$user->isSubscribedTo($plan);

Get Cashier Subscription

Because Spike uses Laravel Cashier (Stripe) or Laravel Cashier (Paddle) under the hood, you can get the underlying Cashier subscription if you would like to interact with it.

$cashierSubscription = $user->getSubscription();

Get subscription renewal date

You can easily get the Carbon\Carbon date of the user's upcoming renewal like so:

$renewsAt = $user->subscriptionRenewalDate();

In case of a yearly subscription, you can get the Carbon\Carbon date of when the monthly credits will be renewed at like so:

$creditsRenewedAt = $user->subscriptionMonthlyRenewalDate();

Support

If you have any questions, feedback, or need any help setting up Spike within your project, feel free to reach out to me.