In Part 1, we designed the data model and backend logic for add-on subscriptions in Laravel Spark. Now it's time to deliver the goods — building the frontend UI and provisioning add-ons when a user clicks "Install".

Delivering the Goods — Provisioning an Add-on

When the user clicks "Install", I want to open a modal where they can:

  • Confirm the plan they have selected
  • Specify which team it should belong to (if they have more than one)
  • Enter their credit card details if they haven't previously provided them

Building the Install Modal

Spark uses Vue.js for its frontend components. We'll extend this pattern for our add-on modal. Create a new component at resources/assets/js/components/addon-install.js:

Vue.component('addon-install', {
    props: ['addon'],
    data() {
        return {
            form: new SparkForm({
                team_id: null,
                stripe_token: null,
            }),
            installStep: 'confirm',
        }
    },
    methods: {
        install() {
            Spark.post('/api/addons/' + this.addon.id + '/install', this.form)
                .then(() => {
                    this.$emit('installed');
                    $('#modal-addon-install').modal('hide');
                });
        }
    }
});

The Install Endpoint

Now let's build the API endpoint that handles the installation request. In your AddonController:

public function install(Request $request, Addon $addon)
{
    $team = Team::findOrFail($request->team_id);

    $this->authorize('install', $addon);

    $subscription = $team->newSubscription($addon->slug, $addon->stripe_plan)
        ->create($request->stripe_token);

    event(new AddonInstalled($team, $addon, $subscription));

    return response()->json(['status' => 'installed']);
}

Fire an event when an add-on is installed. This keeps your controller thin and lets you attach listeners — sending welcome emails, provisioning resources, logging installs — without cluttering your controller logic.

Handling the AddonInstalled Event

Create a listener that provisions whatever the add-on provides. For example, if the add-on unlocks extra storage:

class ProvisionAddonStorage
{
    public function handle(AddonInstalled $event)
    {
        $event->team->update([
            'storage_limit' => $event->team->storage_limit + $event->addon->storage_gb,
        ]);
    }
}

The Vue Modal Template

Register the modal in your main layout. Spark uses Bootstrap modals, so we'll follow that convention:

<addon-install
    :addon="selectedAddon"
    @installed="onAddonInstalled">
</addon-install>

Handling Webhooks for Add-ons

Stripe sends webhook events when subscriptions renew, fail, or are cancelled. Extend Spark's webhook controller to handle your add-on subscriptions:

protected function handleCustomerSubscriptionDeleted(array $payload)
{
    $subscription = Subscription::where('stripe_id', $payload['data']['object']['id'])->first();

    if ($subscription && $subscription->isAddon()) {
        event(new AddonCancelled($subscription->team, $subscription->addon));
    }

    parent::handleCustomerSubscriptionDeleted($payload);
}

With these pieces in place, you have a fully functional add-on subscription system built on top of Laravel Spark. Users can install and uninstall add-ons, teams are billed correctly, and your event system keeps everything in sync.