Skip to content

Add subscription data section in account settings #1838

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/host/app/components/matrix/payment-setup.gts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export default class PaymentSetup extends Component<Signature> {
<BoxelButton
@as='anchor'
@kind='primary'
@disabled={{this.billingService.fetchingStripePaymentLinks}}
@href={{this.stripePaymentLink}}
data-test-setup-payment
class='setup-button'
Expand Down
171 changes: 52 additions & 119 deletions packages/host/app/components/operator-mode/profile-info-popover.gts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,10 @@ import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import Component from '@glimmer/component';

import { task } from 'ember-concurrency';
import { Avatar, BoxelButton } from '@cardstack/boxel-ui/components';
import { cn, or } from '@cardstack/boxel-ui/helpers';

import {
Avatar,
BoxelButton,
LoadingIndicator,
} from '@cardstack/boxel-ui/components';
import { cn } from '@cardstack/boxel-ui/helpers';
import { IconHexagon } from '@cardstack/boxel-ui/icons';
import WithSubscriptionData from '@cardstack/host/components/with-subscription-data';

import BillingService from '@cardstack/host/services/billing-service';
import MatrixService from '@cardstack/host/services/matrix-service';
Expand Down Expand Up @@ -137,127 +132,65 @@ export default class ProfileInfoPopover extends Component<ProfileInfoPopoverSign
</header>

<ProfileInfo />
{{! Show credit info if the user has an active plan }}
{{#if this.plan}}
<div class='credit-info' data-test-credit-info>
<div class='info-group'>
<span class='label'>Membership Tier</span>
<span class='value' data-test-membership-tier>
{{#if this.isLoading}}
<LoadingIndicator />
{{else}}
{{this.plan}}
{{/if}}
</span>
</div>
<BoxelButton
@kind='secondary-light'
@size='small'
@disabled={{this.isLoading}}
data-test-upgrade-plan-button
{{on 'click' @toggleProfileSettings}}
>Upgrade Plan</BoxelButton>
<div class='info-group'>
<span class='label'>Monthly Credit</span>
<span
class={{cn 'value' out-of-credit=this.isOutOfPlanCreditAllowance}}
data-test-monthly-credit
>
{{#if this.isLoading}}
<LoadingIndicator />
{{else}}
<IconHexagon width='16px' height='16px' />
{{this.monthlyCreditText}}
{{/if}}
</span>
</div>
<div class='info-group additional-credit'>
<span class='label'>Additional Credit</span>
<span
class={{cn 'value' out-of-credit=this.isOutOfCredit}}
data-test-additional-credit
>{{#if this.isLoading}}
<LoadingIndicator />
{{else}}
<IconHexagon width='16px' height='16px' />
{{this.extraCreditsAvailableInBalance}}
{{/if}}</span>
</div>
<div
class={{cn 'buy-more-credits' out-of-credit=this.isOutOfCredit}}
data-test-buy-more-credits
>
<WithSubscriptionData as |subscriptionData|>
{{! Show credit info if the user has an active plan }}
{{#if subscriptionData.hasActiveSubscription}}
<div class='credit-info' data-test-credit-info>
<div class='info-group'>
<span class='label'>Membership Tier</span>
{{subscriptionData.plan}}
</div>
<BoxelButton
@kind={{if this.isOutOfCredit 'primary' 'secondary-light'}}
@size={{if this.isOutOfCredit 'base' 'small'}}
@disabled={{this.isLoading}}
{{on 'click' @toggleProfileSettings}}
>Buy more credits</BoxelButton>
@as='anchor'
@kind='secondary-light'
@size='small'
@disabled={{or
subscriptionData.isLoading
this.billingService.fetchingStripePaymentLinks
}}
@href={{this.billingService.customerPortalLink.url}}
target='_blank'
data-test-upgrade-plan-button
>Upgrade Plan</BoxelButton>
<div class='info-group'>
<span class='label'>Monthly Credit</span>
{{subscriptionData.monthlyCredit}}
</div>
<div class='info-group additional-credit'>
<span class='label'>Additional Credit</span>
{{subscriptionData.additionalCredit}}
</div>
<div
class={{cn
'buy-more-credits'
out-of-credit=subscriptionData.isOutOfCredit
}}
data-test-buy-more-credits
>
<BoxelButton
@kind={{if
subscriptionData.isOutOfCredit
'primary'
'secondary-light'
}}
@size={{if subscriptionData.isOutOfCredit 'base' 'small'}}
@disabled={{subscriptionData.isLoading}}
{{on 'click' @toggleProfileSettings}}
>Buy more credits</BoxelButton>
</div>
</div>
</div>
{{/if}}
{{/if}}
</WithSubscriptionData>

</div>
</template>

constructor(...args: [any, any]) {
super(...args);
this.fetchCreditInfo.perform();
}

@service private declare billingService: BillingService;
@service declare matrixService: MatrixService;

private fetchCreditInfo = task(async () => {
await this.billingService.fetchSubscriptionData();
});
@service declare billingService: BillingService;

@action private logout() {
this.matrixService.logout();
}

private get isLoading() {
return this.billingService.fetchingSubscriptionData;
}

private get plan() {
return this.billingService.subscriptionData?.plan;
}

private get creditsIncludedInPlanAllowance() {
return this.billingService.subscriptionData?.creditsIncludedInPlanAllowance;
}

private get creditsAvailableInPlanAllowance() {
return this.billingService.subscriptionData
?.creditsAvailableInPlanAllowance;
}

private get extraCreditsAvailableInBalance() {
return this.billingService.subscriptionData?.extraCreditsAvailableInBalance;
}

private get monthlyCreditText() {
return this.creditsAvailableInPlanAllowance != null &&
this.creditsIncludedInPlanAllowance != null
? `${this.creditsAvailableInPlanAllowance} of ${this.creditsIncludedInPlanAllowance} left`
: null;
}

private get isOutOfCredit() {
return (
this.isOutOfPlanCreditAllowance &&
(this.extraCreditsAvailableInBalance == null ||
this.extraCreditsAvailableInBalance == 0)
);
}

private get isOutOfPlanCreditAllowance() {
return (
this.creditsAvailableInPlanAllowance == null ||
this.creditsIncludedInPlanAllowance == null ||
this.creditsAvailableInPlanAllowance <= 0
);
}
}

export class ProfileInfo extends Component<ProfileInfoSignature> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { isValidPassword } from '@cardstack/host/lib/matrix-utils';
import MatrixService from '@cardstack/host/services/matrix-service';

import ProfileEmail from './profile-email';
import ProfileSubscription from './profile-subscription';

interface Signature {
Args: {
Expand Down Expand Up @@ -126,6 +127,7 @@ export default class ProfileSettingsModal extends Component<Signature> {
@changeEmailComplete={{this.completeEmail}}
/>
{{/if}}
<ProfileSubscription />
</form>
{{#if (or (bool this.displayNameError) (bool this.error))}}
<div class='error-message' data-test-profile-save-error>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { service } from '@ember/service';
import Component from '@glimmer/component';

import {
BoxelButton,
FieldContainer,
LoadingIndicator,
} from '@cardstack/boxel-ui/components';
import { IconHexagon } from '@cardstack/boxel-ui/icons';

import WithSubscriptionData from '@cardstack/host/components/with-subscription-data';
import BillingService from '@cardstack/host/services/billing-service';

interface Signature {
Args: {};
Element: HTMLElement;
}

export default class ProfileSubscription extends Component<Signature> {
<template>
<WithSubscriptionData as |subscriptionData|>
<FieldContainer
@label='Membership Tier'
@tag='label'
class='profile-field'
>
<div class='profile-subscription'>
<div class='monthly-credit'>
<div class='plan-name'>{{subscriptionData.plan}}</div>
<div class='credit-info'>
<span class='credit-info__label'>Monthly Credit</span>
{{subscriptionData.monthlyCredit}}
</div>
</div>
<BoxelButton
@as='anchor'
@kind='secondary-light'
@size='extra-small'
@disabled={{this.billingService.fetchingStripePaymentLinks}}
@href={{this.billingService.customerPortalLink.url}}
target='_blank'
data-test-manage-plan-button
>Manage Plan</BoxelButton>
</div>
</FieldContainer>
<FieldContainer
@label='Additional Credit'
@tag='label'
class='profile-field'
>
<div class='additional-credit'>
<div class='profile-subscription'>
<div class='credit-info'>
{{subscriptionData.additionalCredit}}
</div>
</div>
<div class='buy-more-credits'>
<span class='buy-more-credits__title'>Buy more credits</span>
<div class='payment-links'>
{{#if this.billingService.fetchingStripePaymentLinks}}
<LoadingIndicator />
{{else}}
{{#each
this.billingService.extraCreditsPaymentLinks
as |paymentLink index|
}}
<div class='payment-link' data-test-payment-link={{index}}>
<span><IconHexagon width='16px' height='16px' />
{{paymentLink.creditReloadAmount}}</span>
<BoxelButton
@as='anchor'
@kind='secondary-light'
@size='extra-small'
@href={{paymentLink.url}}
target='_blank'
data-test-pay-button={{index}}
>Pay</BoxelButton>
</div>
{{/each}}
{{/if}}
</div>
</div>
</div>
</FieldContainer>
</WithSubscriptionData>
<style scoped>
.profile-field :deep(.invalid) {
box-shadow: none;
}
.profile-field + .profile-field {
margin-top: var(--boxel-sp-xl);
}
.profile-subscription {
display: flex;
justify-content: space-between;
}
.monthly-credit {
display: flex;
flex-direction: column;
gap: var(--boxel-sp);
}
.credit-info {
display: flex;
flex-direction: column;
gap: var(--boxel-sp-xs);
padding-left: var(--boxel-sp-sm);
border-left: 5px solid #c6c6c6;
min-height: 40px;
}
.credit-info__label {
font: var(--boxel-font-xs);
letter-spacing: var(--boxel-lsp-xs);
text-wrap: nowrap;
line-height: 18px;
}
.additional-credit {
display: flex;
flex-direction: column;
gap: var(--boxel-sp);
}
.buy-more-credits {
display: flex;
flex-direction: column;
gap: var(--boxel-sp-sm);
border-top: 1px solid var(--boxel-300);
padding-top: var(--boxel-sp-sm);
}
.buy-more-credits__title {
font: 600 var(--boxel-font-sm);
}
.payment-links {
display: flex;
flex-direction: column;
gap: var(--boxel-sp-sm);
padding-left: var(--boxel-sp-xs);
}
.payment-link {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--boxel-300);
padding: var(--boxel-sp-xxs);
}
.payment-link > span {
color: var(--boxel-dark);
font: 600 var(--boxel-font-sm);
display: flex;
align-items: center;
gap: var(--boxel-sp-4xs);

--icon-color: var(--boxel-teal);
--boxel-loading-indicator-size: var(--boxel-icon-xs);
}
:deep(.buy-more-credits .boxel-loading-indicator) {
width: 100%;
text-align: center;
}
</style>
</template>

@service private declare billingService: BillingService;
}
Loading