diff --git a/app/Http/Middleware/App/HandleInertiaRequests.php b/app/Http/Middleware/App/HandleInertiaRequests.php index da9300ed..1ffcb50f 100644 --- a/app/Http/Middleware/App/HandleInertiaRequests.php +++ b/app/Http/Middleware/App/HandleInertiaRequests.php @@ -4,6 +4,8 @@ namespace App\Http\Middleware\App; +use App\Http\Resources\App\HandleInertiaRequests\AuthAccountResource; +use App\Http\Resources\App\HandleInertiaRequests\AuthPlanResource; use App\Http\Resources\App\HandleInertiaRequests\AuthUserResource; use App\Http\Resources\App\HandleInertiaRequests\AuthWorkspaceResource; use App\Models\Account; @@ -40,7 +42,8 @@ public function share(Request $request): array 'workspaces' => $user ? $user->workspaces()->with('media')->get()->map(fn ($ws) => AuthWorkspaceResource::summary($ws)) : [], - 'plan' => $account?->plan, + 'account' => $account ? AuthAccountResource::make($account) : null, + 'plan' => $account && $account->plan ? AuthPlanResource::make($account, $account->plan) : null, 'hasActiveSubscription' => $account ? $account->hasActiveSubscription() : false, 'currentPriceId' => $account?->subscription(Account::SUBSCRIPTION_NAME)?->stripe_price, ], diff --git a/app/Http/Resources/App/HandleInertiaRequests/AuthAccountResource.php b/app/Http/Resources/App/HandleInertiaRequests/AuthAccountResource.php new file mode 100644 index 00000000..84e44053 --- /dev/null +++ b/app/Http/Resources/App/HandleInertiaRequests/AuthAccountResource.php @@ -0,0 +1,22 @@ + + */ + public static function make(Account $account): array + { + return [ + 'id' => $account->id, + 'name' => $account->name, + 'created_at' => $account->created_at, + ]; + } +} diff --git a/app/Http/Resources/App/HandleInertiaRequests/AuthPlanResource.php b/app/Http/Resources/App/HandleInertiaRequests/AuthPlanResource.php new file mode 100644 index 00000000..b500fe14 --- /dev/null +++ b/app/Http/Resources/App/HandleInertiaRequests/AuthPlanResource.php @@ -0,0 +1,29 @@ + + */ + public static function make(Account $account, Plan $plan): array + { + $subscription = $account->subscription(Account::SUBSCRIPTION_NAME); + $interval = ($subscription && $subscription->stripe_price === $plan->stripe_yearly_price_id) + ? 'yearly' + : 'monthly'; + + return [ + 'id' => $plan->id, + 'slug' => $plan->slug->value, + 'name' => $plan->name, + 'interval' => $interval, + ]; + } +} diff --git a/resources/js/app.ts b/resources/js/app.ts index f2e142a8..994667e5 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -8,6 +8,7 @@ import type { DefineComponent } from 'vue'; import { createApp, h } from 'vue'; import { initializeTheme } from './composables/useAppearance'; +import { initializeDataLayer } from './datalayer'; import dayjs from './dayjs'; import posthog from './posthog'; import type { Auth } from './types'; @@ -33,6 +34,16 @@ createInertiaApp({ dayjs.locale(locale.toLowerCase()); const auth = props.initialPage.props.auth as Auth | undefined; + const flash = props.initialPage.props.flash as + | { conversion_event?: string; [key: string]: unknown } + | undefined; + + initializeDataLayer( + auth, + flash, + props.initialPage.props.applicationUrl as string, + props.initialPage.props.env as string, + ); if (auth?.user) { posthog.identify(auth.user.id, { diff --git a/resources/js/composables/useTracking.ts b/resources/js/composables/useTracking.ts index c4f3ff6e..da363f02 100644 --- a/resources/js/composables/useTracking.ts +++ b/resources/js/composables/useTracking.ts @@ -17,34 +17,28 @@ export const useTracking = () => ({ }); }, - trackBeginCheckout: (plan: { name: string; price: number; interval: string }) => { + trackBeginCheckout: (plan: { name: string; interval: string }) => { posthog.capture('checkout.started', { plan_name: plan.name, - plan_price: plan.price, interval: plan.interval, }); push({ event: 'begin_checkout', - currency: 'USD', plan_name: plan.name, - plan_price: plan.price, plan_interval: plan.interval, }); }, - trackPurchase: (plan: { name: string; price: number; interval: string }) => { + trackPurchase: (plan: { name: string; interval: string }) => { posthog.capture('checkout.completed', { plan_name: plan.name, - plan_price: plan.price, interval: plan.interval, }); push({ event: 'purchase', - currency: 'USD', plan_name: plan.name, - plan_price: plan.price, plan_interval: plan.interval, }); }, diff --git a/resources/js/datalayer.ts b/resources/js/datalayer.ts new file mode 100644 index 00000000..37160fcc --- /dev/null +++ b/resources/js/datalayer.ts @@ -0,0 +1,64 @@ +import type { Auth } from './types'; + +interface FlashData { + conversion_event?: string; + [key: string]: unknown; +} + +/** + * Push app + identity context to GTM's dataLayer on every page load. The + * pushes are no-ops when GTM isn't configured (the array still exists in + * memory; nothing reads it). Self-hosted instances without GTM_ID set + * incur zero error noise. + * + * Billing is account-scoped (not workspace-scoped) in trypost, so the + * plan/subscription fields are emitted under `account_*` keys. + */ +export const initializeDataLayer = ( + auth: Auth | undefined, + flash: FlashData | undefined, + applicationUrl: string, + env: string, +): void => { + window.dataLayer = window.dataLayer || []; + + window.dataLayer.push({ + app_url: applicationUrl, + app_env: env, + app_context: 'app', + }); + + if (flash?.conversion_event) { + window.dataLayer.push({ event: flash.conversion_event }); + } + + if (!auth?.user) { + return; + } + + window.dataLayer.push({ + user_id: auth.user.id, + user_email: auth.user.email, + user_name: auth.user.name, + user_created_at: auth.user.created_at, + }); + + if (auth.account) { + window.dataLayer.push({ + account_id: auth.account.id, + account_name: auth.account.name, + account_created_at: auth.account.created_at, + account_plan: auth.plan?.name ?? null, + account_plan_slug: auth.plan?.slug ?? null, + account_subscribed: Boolean(auth.hasActiveSubscription), + }); + } + + if (auth.currentWorkspace) { + window.dataLayer.push({ + workspace_id: auth.currentWorkspace.id, + workspace_name: auth.currentWorkspace.name, + workspace_count: auth.workspaces?.length ?? 0, + }); + } +}; diff --git a/resources/js/pages/billing/Processing.vue b/resources/js/pages/billing/Processing.vue index cf89327b..d1a60087 100644 --- a/resources/js/pages/billing/Processing.vue +++ b/resources/js/pages/billing/Processing.vue @@ -1,28 +1,60 @@