freekmurze / freek-dev-comments

2 stars 0 forks source link

1912-how-to-customize-jetstream-and-laravel-spark #75

Open utterances-bot opened 3 years ago

utterances-bot commented 3 years ago

How to customize Jetstream and Laravel Spark - Freek Van der Herten's blog on PHP, Laravel and JavaScript

https://freek.dev/1912-how-to-customize-jetstream-and-laravel-spark

thomasrd1 commented 3 years ago

Great write up Freek.

I'm interested to understand how you are managing the feature limits per plan. Did you put your feature/product limits in the billables or have a separate reference somewhere else?

'billables' => [
        'customers' => [
            'model'      => Customer::class,
            //'trial_days' => env('BILLING_SPARK_TRIAL_DAYS', 7),
            'plans'      => [
                [
                    'name'              => 'Basic Plan',
                    'short_description' => 'This is a short, human friendly description of the plan.',
                    'monthly_id'        => env('BILLING_SPARK_PLAN1_STRIPEID_MONTHLY'),
                    'yearly_id'         => env('BILLING_SPARK_PLAN1_STRIPEID_YEARLY'),
                    'trial_days'        => env('BILLING_SPARK_PLAN1_TRIAL_DAYS'),
                    'features'          => [
                        'Feature 1',
                        'Feature 2',
                    ],
                    'options' => [
                        'package_integrations_count' => '1',
                        'package_product_enrich' => '0',
                        'package_credits_per' => '10000',
                    ],
                ],
                [
                    'name'              => 'Standard Plan',
                    'short_description' => 'This is a short, human friendly description of the plan.',
                    'monthly_id'        => env('BILLING_SPARK_PLAN2_STRIPEID_MONTHLY'),
                    'yearly_id'         => env('BILLING_SPARK_PLAN2_STRIPEID_YEARLY'),
                    // 'trial_days'        => env('BILLING_SPARK_PLAN2_TRIAL_DAYS'),
                    'features'          => [
                        'Feature 1',
                        'Feature 2',
                    ],
                    'options' => [
                        'package_integrations_count' => '100',
                        'package_product_enrich' => '1',
                        'package_credits_per' => '50000',
                    ],
                ],
            ],
        ],
    ]
freekmurze commented 3 years ago

Thanks 🙂

I do have a class that registers plans with the spark config. By setting it up this way I can easily use the findForStripePlanName method to get the exact plan with the allowed features.

<?php

namespace App\Domain\Subscription\Support\SparkPlans;

use App\Domain\Subscription\Support\SparkPlans\Plans\Enterprise;
use App\Domain\Subscription\Support\SparkPlans\Plans\Mini;
use App\Domain\Subscription\Support\SparkPlans\Plans\Plus;
use App\Domain\Subscription\Support\SparkPlans\Plans\Premium;
use App\Domain\Subscription\Support\SparkPlans\Plans\Pro;
use App\Domain\Subscription\Support\SparkPlans\Plans\Standard;
use App\Domain\Subscription\Support\SparkPlans\Plans\Starter;
use Illuminate\Support\Collection;

class PlanRepository
{
    public static function getAll(): Collection
    {
        return collect([
            new Starter(),
            new Mini(),
            new Standard(),
            new Plus(),
            new Pro(),
            new Enterprise(),
            new Premium(),
        ]);
    }

    public static function findForStripePlanName(string $stripePlanName): ?SparkPlan
    {
        return self::getAll()
            ->first(fn (SparkPlan $sparkPlan) => in_array($stripePlanName, [
                $sparkPlan->monthlyStripeName(),
                $sparkPlan->yearlyStripeName(),
            ]));
    }

    public static function registerWithConfig()
    {
        static::getAll()
            ->map(function (SparkPlan $plan) {
                return [
                    'name' => $plan->name(),
                    'short_description' => 'Uptime, broken links, mixed content and scheduled jobs monitoring, and status pages',
                    'monthly_id' => $plan->monthlyStripeName(),
                    'yearly_id' => $plan->yearlyStripeName(),
                    'monthly_incentive' => $plan->monthlyIncentive(),
                    'yearly_incentive' => $plan->yearlyIncentive(),
                    'features' => [
                        "Monitor {$plan->maximumNumberOfSites()} sites",
                    ],
                    'archived' => $plan->archived(),
                ];
            })
            ->pipe(function (Collection $allPlants) {
                config()->set('spark.billables.user.plans', $allPlants->toArray());
            });
    }
}

Here is one of the plan classes:

<?php

namespace App\Domain\Subscription\Support\SparkPlans\Plans;

use App\Domain\Subscription\Support\SparkPlans\SparkPlan;

class Starter extends SparkPlan
{
    public function name(): string
    {
        return 'Starter';
    }

    public function monthlyStripeName(): string
    {
        return config('services.stripe.plans.starter.monthly');
    }

    public function yearlyStripeName(): string
    {
        return config('services.stripe.plans.starter.yearly');
    }

    public function maximumNumberOfSites(): int
    {
        return 2;
    }

    public function features(): array
    {
        return ['2 sites'];
    }

    public function archived(): bool
    {
        return true;
    }
}

And here is the base plan class:

<?php

namespace App\Domain\Subscription\Support\SparkPlans;

abstract class SparkPlan
{
    abstract public function name(): string;

    abstract public function monthlyStripeName(): string;

    abstract public function yearlyStripeName(): string;

    abstract public function maximumNumberOfSites(): int;

    abstract public function features(): array;

    public function archived(): bool
    {
        return false;
    }

    public function maximumNumberOfCronCheckDefinitions(): int
    {
        return $this->maximumNumberOfSites() * 5;
    }

    public function monthlyIncentive(): string
    {
        return '';
    }

    public function yearlyIncentive(): string
    {
        return 'Save 10%';
    }
}

On my team model I have this function to retrieve the plan:

    public function getActiveSparkPlan(): ?SparkPlan
    {
        $subscription = $this->getActiveSubscription();

        if (! $subscription) {
            return null;
        }

        return PlanRepository::findForStripePlanName($subscription->stripe_plan);
    }
Braunson commented 3 years ago

Great article Freek, I did notice an "error".

On the line Here, I've invited "john@spatie.be" to the Spatie team. the image is the same one from the previous image from the previous section. :)

lsmith77 commented 2 years ago

It seems like Spark, just like Classic Spark, generates its own invoices rather than using the invoices generated by Stripe. This always seemed risky to me and the point you make about a change of address is a good example of such risks. But other than this customization you are happy using the Spark generated invoices?

rossco555 commented 2 years ago

Hi, thanks for sharing - I get a log from your blogs. I'm fairly beginner level and trying to modify Jestream as you described to not create a personal team for every new user. I was trying to follow your EnsureHasTeams middleware but I notice there is currentUser() used that I would need to do something to setup. Could you please help me to know how you define currentUser()? I tried to just replace with auth()->user() but this doesn't seem to work.

BWMURPHY commented 2 years ago

Publishing the views doesn't publish the BillingPortal.vue file, which is located in the js/pages folder, not in the views folder. Publishing the views only publishes the very limited spark.php file.

I can't find a publish command anywhere that exposes the vue file in the js/pages folder for editing to accomplish what you did above.

Any suggestions on how to publish the vue file in the pages folder?