From 4ed619a30094ef37eb20a272df071630e04583b4 Mon Sep 17 00:00:00 2001 From: James Kiger Date: Tue, 18 Mar 2025 08:48:48 -0400 Subject: [PATCH] Use router input for org slug and query params in subscription component, adjust subscription refresh handling --- .eslintrc.json | 5 +- src/app/api/organizations.service.ts | 1 + .../subscriptions/subscriptions.service.ts | 119 ++++--- .../subscription/payment/payment.component.ts | 8 +- .../subscription/subscription.component.html | 336 +++++++++++------- .../subscription/subscription.component.ts | 67 ++-- 6 files changed, 299 insertions(+), 237 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index b2258f47..573d3276 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -34,7 +34,8 @@ "prefix": "gt", "style": "kebab-case" } - ] + ], + "@angular-eslint/no-input-rename": "off" } }, { @@ -50,4 +51,4 @@ "extends": [ "plugin:storybook/recommended" ] -} \ No newline at end of file +} diff --git a/src/app/api/organizations.service.ts b/src/app/api/organizations.service.ts index 4f0a35ce..c6cdedc9 100644 --- a/src/app/api/organizations.service.ts +++ b/src/app/api/organizations.service.ts @@ -36,6 +36,7 @@ export class OrganizationsService { activeOrganizationResource = resource({ request: () => ({ organization_slug: this.activeOrganizationSlug() }), loader: async ({ request }) => { + console.log("loading for some reasn") if (!request.organization_slug) { return undefined; } diff --git a/src/app/api/subscriptions/subscriptions.service.ts b/src/app/api/subscriptions/subscriptions.service.ts index 4d9bdd90..4d616f64 100644 --- a/src/app/api/subscriptions/subscriptions.service.ts +++ b/src/app/api/subscriptions/subscriptions.service.ts @@ -1,4 +1,4 @@ -import { computed, Injectable, inject, resource } from "@angular/core"; +import { computed, Injectable, inject, resource, signal } from "@angular/core"; import { Router } from "@angular/router"; import { lastValueFrom } from "rxjs"; import { tap } from "rxjs/operators"; @@ -6,12 +6,12 @@ import { EventsCount } from "./subscriptions.interfaces"; import { StatefulService } from "src/app/shared/stateful-service/signal-state.service"; import { SubscriptionsAPIService } from "./subscriptions-api.service"; import { client } from "../api"; -import { OrganizationsService } from "../organizations.service"; import { SettingsService } from "../settings.service"; interface SubscriptionsState { billingPortalLoading: boolean; billingPortalLoadingError: string; + subscriptionRefreshing: boolean; subscriptionLoadingTimeout: boolean; fromStripe: boolean; eventsCount: EventsCount | null; @@ -20,6 +20,7 @@ interface SubscriptionsState { const initialState: SubscriptionsState = { billingPortalLoading: false, billingPortalLoadingError: "", + subscriptionRefreshing: false, subscriptionLoadingTimeout: false, fromStripe: false, eventsCount: null, @@ -29,20 +30,17 @@ const initialState: SubscriptionsState = { providedIn: "root", }) export class SubscriptionsService extends StatefulService { - private organizationsService = inject(OrganizationsService); private settingsService = inject(SettingsService); private subscriptionsAPIService = inject(SubscriptionsAPIService); private router = inject(Router); stripePublicKey = this.settingsService.stripePublicKey; - subscriptionLoading = computed(() => this.subscriptionResource.isLoading()); - subscriptionLoadingTimeout = computed( - () => this.state().subscriptionLoadingTimeout - ); + organizationSlug = signal(""); + subscriptionResource = resource({ request: () => ({ - orgSlug: this.organizationsService.activeOrganizationSlug(), + orgSlug: this.organizationSlug(), }), loader: async ({ request }) => { if (!request.orgSlug) { @@ -74,10 +72,18 @@ export class SubscriptionsService extends StatefulService { }; return formattedSubscription; }); + subscriptionLoading = computed( + () => + this.subscriptionResource.isLoading() || + this.state().subscriptionRefreshing + ); + subscriptionLoadingTimeout = computed( + () => this.state().subscriptionLoadingTimeout + ); eventCountResource = resource({ request: () => ({ - orgSlug: this.organizationsService.activeOrganizationSlug(), + orgSlug: this.organizationSlug(), }), loader: async ({ request }) => { if (!request.orgSlug) { @@ -98,7 +104,6 @@ export class SubscriptionsService extends StatefulService { return data; }, }); - eventsCountWithTotal = computed(() => { const eventsCount = this.eventCountResource.value(); if (!eventsCount) return eventsCount; @@ -116,7 +121,6 @@ export class SubscriptionsService extends StatefulService { billingPortalLoadingError = computed( () => this.state().billingPortalLoadingError ); - fromStripe = computed(() => this.state().fromStripe); totalEventsAllowed = computed(() => { const subscription = this.subscription(); @@ -127,33 +131,11 @@ export class SubscriptionsService extends StatefulService { super(initialState); } - /** - * Keep trying to get subscription, for users redirected from Stripe - * @param slug Organization Slug for requested subscription - */ - // retrieveUntilSubscriptionOrTimeout(slug: string) { - // this.setSubscriptionLoadingStart(true); - // lastValueFrom( - // this.subscriptionsAPIService.retrieve(slug).pipe( - // expand((subscription) => { - // if (!subscription.created) { - // return this.subscriptionsAPIService - // .retrieve(slug) - // .pipe(delay(2000)); - // } else { - // this.setSubscription(subscription); - // return EMPTY; - // } - // }), - // catchError(() => { - // this.setSubscriptionLoadingError(); - // return EMPTY; - // }), - // takeUntil(this.subscriptionRetryTimer()) - // ), - // { defaultValue: null } - // ); - // } + retrieveSubscriptionData(orgSlug: string) { + if (orgSlug) { + this.organizationSlug.set(orgSlug); + } + } /** * Retrieve Subscription and navigate to subscription page if no subscription exists @@ -182,7 +164,7 @@ export class SubscriptionsService extends StatefulService { async redirectToBillingPortal() { this.setBillingPortalLoadingStart(); - const orgSlug = this.organizationsService.activeOrganizationSlug(); + const orgSlug = this.organizationSlug(); const { data, error } = await client.POST( "/api/0/stripe/organizations/{organization_slug}/create-billing-portal/", { @@ -201,36 +183,55 @@ export class SubscriptionsService extends StatefulService { } } - // private subscriptionRetryTimer() { - // return timer(60000).pipe( - // tap(() => { - // this.setSubscriptionLoadingTimeout(); - // }) - // ); - // } + /** + * Keep trying to get subscription, for users redirected from Stripe + */ + refreshUntilSubscriptionOrTimeout() { + this.setSubscriptionRefreshingStart(true); + let i = 0; + const intervalRef = setInterval(() => { + this.subscriptionResource.reload(); + if (this.subscription()) { + this.setSubscriptionRefreshingComplete(); + clearInterval(intervalRef); + } else if (i === 2) { + this.setSubscriptionRefreshingTimeout(); + clearInterval(intervalRef); + } + i++; + }, 2000); + } - // private setSubscriptionLoadingStart(fromStripe: boolean = false) { - // this.setState({ subscriptionLoading: true, fromStripe }); - // } + private setSubscriptionRefreshingStart(fromStripe: boolean = false) { + this.setState({ subscriptionRefreshing: true, fromStripe }); + } - // private setSubscriptionLoadingError() { - // this.setState({ subscriptionLoading: false }); - // } + private setSubscriptionRefreshingComplete(fromStripe: boolean = false) { + this.setState({ subscriptionRefreshing: false }); + } - // private setSubscriptionLoadingTimeout() { - // this.setState({ - // subscriptionLoadingTimeout: true, - // }); - // } + private setSubscriptionRefreshingTimeout() { + this.setState({ + subscriptionRefreshing: false, + subscriptionLoadingTimeout: true, + }); + } - setBillingPortalLoadingStart() { + private setBillingPortalLoadingStart() { this.setState({ billingPortalLoading: true }); } - setBillingPortalLoadingError(message: string) { + private setBillingPortalLoadingError(message: string) { this.setState({ billingPortalLoading: false, billingPortalLoadingError: message, }); } + + clearState() { + super.clearState(); + this.organizationSlug.set(""); + this.subscriptionResource.set(undefined); + this.eventCountResource.set(undefined); + } } diff --git a/src/app/settings/subscription/payment/payment.component.ts b/src/app/settings/subscription/payment/payment.component.ts index f81a5139..23449100 100644 --- a/src/app/settings/subscription/payment/payment.component.ts +++ b/src/app/settings/subscription/payment/payment.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, OnInit, inject } from "@angular/core"; +import { Component, ChangeDetectionStrategy, inject } from "@angular/core"; import { environment } from "../../../../environments/environment"; import { EventInfoComponent } from "../../../shared/event-info/event-info.component"; import { MatDividerModule } from "@angular/material/divider"; @@ -23,7 +23,7 @@ import { OrganizationsService } from "src/app/api/organizations.service"; DecimalPipe, ], }) -export class PaymentComponent implements OnInit { +export class PaymentComponent { private organizationService = inject(OrganizationsService); private paymentService = inject(PaymentService); @@ -32,10 +32,6 @@ export class PaymentComponent implements OnInit { this.paymentService.subscriptionCreationLoadingId; billingEmail = environment.billingEmail; - ngOnInit() { - this.organizationService.activeOrganizationResource.reload(); - } - onSubmit(price: Price) { const activeOrganization = this.organizationService.activeOrganization(); if (activeOrganization) { diff --git a/src/app/settings/subscription/subscription.component.html b/src/app/settings/subscription/subscription.component.html index 8132920e..2e20d4cb 100644 --- a/src/app/settings/subscription/subscription.component.html +++ b/src/app/settings/subscription/subscription.component.html @@ -2,142 +2,212 @@ - @if (!(subscriptionLoading())) { @if (subscription(); as subscription) { - - -
- @if ( subscription.status === "active" || subscription.status === - "trialing" || subscription.status === "past_due" ) { @if - (subscription.status === "active") { -

- @if (subscription.effectivePrice === 0) { - You have selected the free plan. Please consider making a monthly - donation - to help keep open source development sustainable. Thank you for - choosing GlitchTip! - } @else { - This organization has an active subscription to GlitchTip. Thank - you! - } -

- } @if (subscription.status === "trialing") { -

This organization has a trial subscription to GlitchTip.

- } @if (subscription.status === "past_due") { -

This organization has a subscription to GlitchTip, but the payment is - past due.

+ @if (subscriptionLoading() || subscriptionLoadingTimeout()) { + + + + @if (subscriptionLoading()) { +
+ +
} - - Details -

{{ subscription.product.name }}

- {{ subscription.product.description }} - @if (eventsPercent(); as eventsPercent) { -
-
- {{ totalEventsAllowed() }} -
-
-
-
-
-
-
- @if (eventsCountWithTotal(); as count) { -
- {{ count.total }} -
-
-
    -
  • - Issue events: - {{ count.eventCount }} -
  • -
  • - Uptime events: - {{ count.uptimeCheckEventCount }} -
  • -
  • - Performance events: - {{ count.transactionEventCount }} -
  • -
  • - File storage: - {{ count.fileSizeMb }} MB -
  • -
  • Total - events - used in billing period: {{ count.total }} -
  • -
-
+ @if (subscriptionLoadingTimeout()) { +
+ GlitchTip was unable to find a subscription for your account. If you + just signed up for a subscription on our payment platform, please + contact us at + {{ billingEmail }}. +
+ } + + + } @else { + @if (subscription(); as subscription) { + + +
+ @if ( + subscription.status === "active" || + subscription.status === "trialing" || + subscription.status === "past_due" + ) { + @if (subscription.status === "active") { +

+ @if (subscription.effectivePrice === 0) { + You have selected the free plan. Please consider making a + monthly + donation + to help keep open source development sustainable. Thank + you for choosing GlitchTip! + } @else { + This organization has an active subscription to + GlitchTip. Thank you! + } +

+ } + @if (subscription.status === "trialing") { +

This organization has a trial subscription to GlitchTip.

+ } + @if (subscription.status === "past_due") { +

This organization has a subscription to GlitchTip, but the + payment is past due.

+ } + + Details +

{{ subscription.product.name }}

+ {{ subscription.product.description }} + @if (eventsPercent(); as eventsPercent) { +
+
+ {{ totalEventsAllowed() }} +
+
+
+
+
+
+
+ @if (eventsCountWithTotal(); as count) { +
+ {{ count.total }} +
+
+
    +
  • + Issue events: + {{ count.eventCount }} +
  • +
  • + Uptime events: + {{ count.uptimeCheckEventCount }} +
  • +
  • + Performance events: + {{ count.transactionEventCount }} +
  • +
  • + File storage: + {{ count.fileSizeMb }} MB +
  • +
  • Total + events + used in billing period: {{ count.total }} +
  • +
+
+ } +
+ } +
    +
  • Start date for this plan: + {{ subscription.startDate | date }}
  • +
  • Plan cost: {{ subscription.effectivePrice | currency }}
  • +
  • + Billing: + @if ( + subscription.collectionMethod === "charge_automatically" + ) { + Automatic + } + @if (subscription.collectionMethod === "send_invoice") { + Send invoice + } +
  • +
  • Billing period: + {{ subscription.currentPeriodStart | date }} to + {{ subscription.currentPeriodEnd | date }}
  • +
+
+

If you have questions about your subscription, please contact + us at {{ billingEmail }}. +

+ {{ billingPortalLoadingError() }} + + } @else { +

This organization does not have an active subscription to + GlitchTip. Pick a plan to get started.

}
- } -
    -
  • Start date for this plan: - {{ subscription.startDate | date }}
  • -
  • Plan cost: {{ subscription.effectivePrice | currency }}
  • -
  • - Billing: @if ( - subscription.collectionMethod === "charge_automatically" ) { - Automatic - } @if (subscription.collectionMethod === "send_invoice") { - Send invoice - } -
  • -
  • Billing period: {{ subscription.currentPeriodStart | date }} to - {{ subscription.currentPeriodEnd | date }}
  • -
- -

If you have questions about your subscription, please contact us at - {{ billingEmail }}. -

- {{ billingPortalLoadingError() }} - - } @else { -

This organization does not have an active subscription to GlitchTip. - Pick a plan to get started.

- } -
-
-
- @if (promptForProject()) { - - -

Now that you have an organization and a subscription, create a - project.

- @if (activeOrganizationSlug(); as slug) { - Create New - Project +
+
+ @if (promptForProject()) { + + +

Now that you have an organization and a subscription, create a + project.

+ @if (activeOrganizationSlug(); as slug) { + Create New Project + } +
+
} - - - } } @if ( (subscriptionLoading() === false && (subscription() ) === null) || - (subscription())!.status === "canceled" ) { - - } } @else { @if ( (subscriptionLoading()) || (subscriptionLoadingTimeout()) ) - { - - - - @if (subscriptionLoading()) { -
- -
- } @if (subscriptionLoadingTimeout()) { -
- GlitchTip was unable to find a subscription for your account. If you - just signed up for a subscription on our payment platform, please - contact us at - {{ billingEmail }}. -
- } -
-
- } } - \ No newline at end of file + } + @if ( + (subscriptionLoading() === false && subscription() === null) || + subscription()!.status === "canceled" + ) { + + } + } + diff --git a/src/app/settings/subscription/subscription.component.ts b/src/app/settings/subscription/subscription.component.ts index 1415c659..e6822e8e 100644 --- a/src/app/settings/subscription/subscription.component.ts +++ b/src/app/settings/subscription/subscription.component.ts @@ -1,8 +1,14 @@ -import { Component, ChangeDetectionStrategy, OnDestroy, computed, inject } from "@angular/core"; -import { ActivatedRoute, RouterLink } from "@angular/router"; +import { + Component, + ChangeDetectionStrategy, + OnDestroy, + computed, + inject, + input, + OnInit, +} from "@angular/core"; +import { RouterLink } from "@angular/router"; import { MatDialog, MatDialogModule } from "@angular/material/dialog"; -import { combineLatest, Subscription } from "rxjs"; -import { map, filter, take } from "rxjs/operators"; import { EventInfoComponent } from "src/app/shared/event-info/event-info.component"; import { environment } from "../../../environments/environment"; import { SubscriptionsService } from "src/app/api/subscriptions/subscriptions.service"; @@ -41,11 +47,16 @@ interface Percentages { DatePipe, ], }) -export class SubscriptionComponent implements OnDestroy { +export class SubscriptionComponent implements OnDestroy, OnInit { private service = inject(SubscriptionsService); - private route = inject(ActivatedRoute); - dialog = inject(MatDialog); private orgService = inject(OrganizationsService); + dialog = inject(MatDialog); + + orgSlug = input.required({ alias: "org-slug" }); + sessionId = input("", { alias: "session_id" }); + billingPortalRedirect = input("", { + alias: "billing_portal_redirect", + }); fromStripe = this.service.fromStripe; subscription = this.service.subscription; @@ -55,8 +66,8 @@ export class SubscriptionComponent implements OnDestroy { totalEventsAllowed = this.service.totalEventsAllowed; activeOrganization = this.orgService.activeOrganization; activeOrganizationSlug = this.orgService.activeOrganizationSlug; - billingPortalLoading = this.service.billingPortalLoading - billingPortalLoadingError = this.service.billingPortalLoadingError + billingPortalLoading = this.service.billingPortalLoading; + billingPortalLoadingError = this.service.billingPortalLoadingError; promptForProject = computed(() => { const status = this.orgService.activeOrganizationLoaded(); @@ -73,7 +84,6 @@ export class SubscriptionComponent implements OnDestroy { return false; } }); - routerSubscription: Subscription; billingEmail = environment.billingEmail; eventsPercent = computed(() => { const eventsAllowed = this.totalEventsAllowed(); @@ -88,32 +98,16 @@ export class SubscriptionComponent implements OnDestroy { }; }); - constructor() { - this.routerSubscription = combineLatest([ - this.route.params, - this.route.queryParams, - ]) - .pipe( - map(([params, queryParams]) => { - const slug = params["org-slug"] as string; - const sessionId = queryParams["session_id"]; - const redirectFromBillingPortal = - queryParams["billing_portal_redirect"]; - return { slug, sessionId, redirectFromBillingPortal }; - }), - filter((routerData) => !!routerData.slug), - take(1) - ) - .subscribe((routerData) => { - if (routerData.sessionId) { - // this.service.retrieveUntilSubscriptionOrTimeout(routerData.slug); - } else { - this.service.subscriptionResource.reload(); - } - if (routerData.redirectFromBillingPortal) { - this.orgService.repeatRefreshOrgDetail(); - } - }); + ngOnInit(): void { + this.orgService.activeOrganizationResource.reload(); + this.service.retrieveSubscriptionData(this.orgSlug()); + + if (this.sessionId()) { + this.service.refreshUntilSubscriptionOrTimeout(); + } + if (this.billingPortalRedirect()) { + this.orgService.repeatRefreshOrgDetail(); + } } openEventInfoDialog() { @@ -127,7 +121,6 @@ export class SubscriptionComponent implements OnDestroy { } ngOnDestroy() { - this.routerSubscription.unsubscribe(); this.service.clearState(); } }