Skip to content

Commit

Permalink
NEOS-1579: add free trial timer to header (#2871)
Browse files Browse the repository at this point in the history
  • Loading branch information
evisdrenova authored Oct 30, 2024
1 parent 6cf9765 commit 40640d8
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 91 deletions.
132 changes: 74 additions & 58 deletions backend/gen/go/protos/mgmt/v1alpha1/user_account.pb.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions backend/protos/mgmt/v1alpha1/user_account.proto
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ message IsAccountStatusValidResponse {
optional uint64 allowed_record_count = 5;
// The current status of the account. Default is valid.
AccountStatus account_status = 6;
// The time when the trial expires
optional google.protobuf.Timestamp trial_expires_at = 7;
}

enum AccountStatus {
Expand Down
24 changes: 20 additions & 4 deletions backend/services/mgmt/v1alpha1/user-account-service/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/nucleuscloud/neosync/backend/internal/neosyncdb"
"github.com/nucleuscloud/neosync/internal/billing"
"github.com/stripe/stripe-go/v79"
"google.golang.org/protobuf/types/known/timestamppb"
)

var (
Expand Down Expand Up @@ -165,6 +166,8 @@ func (s *Service) IsAccountStatusValid(
var description string
isValid := false

var trialExpiryDate *timestamppb.Timestamp

switch accountStatusResp.Msg.GetSubscriptionStatus() {
case mgmtv1alpha1.BillingStatus_BILLING_STATUS_EXPIRED:
accountStatus = mgmtv1alpha1.AccountStatus_ACCOUNT_STATUS_ACCOUNT_IN_EXPIRED_STATE
Expand All @@ -175,15 +178,28 @@ func (s *Service) IsAccountStatusValid(
case mgmtv1alpha1.BillingStatus_BILLING_STATUS_TRIAL_ACTIVE:
accountStatus = mgmtv1alpha1.AccountStatus_ACCOUNT_STATUS_ACCOUNT_TRIAL_ACTIVE
isValid = true

accountUuid, err := neosyncdb.ToUuid(req.Msg.GetAccountId())
if err != nil {
return nil, err
}

acc, err := s.db.Q.GetAccount(ctx, s.db.Db, accountUuid)
if err != nil {
return nil, err
}

expiryTime := acc.CreatedAt.Time.Add(trialDuration)
trialExpiryDate = timestamppb.New(expiryTime)
case mgmtv1alpha1.BillingStatus_BILLING_STATUS_ACTIVE:
accountStatus = mgmtv1alpha1.AccountStatus_ACCOUNT_STATUS_REASON_UNSPECIFIED
isValid = true
}

return connect.NewResponse(&mgmtv1alpha1.IsAccountStatusValidResponse{
IsValid: isValid,
AccountStatus: accountStatus,
Reason: &description,
IsValid: isValid,
AccountStatus: accountStatus,
Reason: &description,
TrialExpiresAt: trialExpiryDate,
}), nil
}

Expand Down
12 changes: 12 additions & 0 deletions docs/openapi/mgmt/v1alpha1/user_account.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1488,6 +1488,13 @@ components:
anyOf:
- required:
- reason
- anyOf:
- required:
- trialExpiresAt
- not:
anyOf:
- required:
- trialExpiresAt
properties:
isValid:
type: boolean
Expand Down Expand Up @@ -1522,6 +1529,11 @@ components:
- title: account_status
description: The current status of the account. Default is valid.
- $ref: '#/components/schemas/mgmt.v1alpha1.AccountStatus'
trialExpiresAt:
allOf:
- title: trial_expires_at
description: The time when the trial expires
- $ref: '#/components/schemas/google.protobuf.Timestamp'
title: IsAccountStatusValidResponse
additionalProperties: false
mgmt.v1alpha1.IsUserInAccountRequest:
Expand Down
2 changes: 1 addition & 1 deletion docs/protos/mgmt/v1alpha1/user_account.proto.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ _**package** mgmt.v1alpha1_


### `IsAccountStatusValidResponse`
<ProtoMessage key={35} message={{"name":"IsAccountStatusValidResponse","longName":"IsAccountStatusValidResponse","fullName":"mgmt.v1alpha1.IsAccountStatusValidResponse","description":"","hasExtensions":false,"hasFields":true,"hasOneofs":true,"extensions":[],"fields":[{"name":"is_valid","description":"","label":"","type":"bool","longType":"bool","fullType":"bool","ismap":false,"isoneof":false,"oneofdecl":"","defaultValue":""},{"name":"reason","description":"If the account is not valid, a reason for why may be provided.","label":"optional","type":"string","longType":"string","fullType":"string","ismap":false,"isoneof":true,"oneofdecl":"_reason","defaultValue":""},{"name":"should_poll","description":"Whether or not the process should decide to continue polling for validitiy updates","label":"","type":"bool","longType":"bool","fullType":"bool","ismap":false,"isoneof":false,"oneofdecl":"","defaultValue":""},{"name":"used_record_count","description":"A count of the currently used records for the current billing period.\nThis may go over the allowed record count depending on when the record count is polled by the metric system.\n@deprecated","label":"","type":"uint64","longType":"uint64","fullType":"uint64","ismap":false,"isoneof":false,"oneofdecl":"","defaultValue":""},{"name":"allowed_record_count","description":"The allowed record count. It will be null if there is no limit.\n@deprecated","label":"optional","type":"uint64","longType":"uint64","fullType":"uint64","ismap":false,"isoneof":true,"oneofdecl":"_allowed_record_count","defaultValue":""},{"name":"account_status","description":"The current status of the account. Default is valid.","label":"","type":"AccountStatus","longType":"AccountStatus","fullType":"mgmt.v1alpha1.AccountStatus","ismap":false,"isoneof":false,"oneofdecl":"","defaultValue":"","typeLink":"/api/mgmt/v1alpha1/user_account.proto#accountstatus"}]}} />
<ProtoMessage key={35} message={{"name":"IsAccountStatusValidResponse","longName":"IsAccountStatusValidResponse","fullName":"mgmt.v1alpha1.IsAccountStatusValidResponse","description":"","hasExtensions":false,"hasFields":true,"hasOneofs":true,"extensions":[],"fields":[{"name":"is_valid","description":"","label":"","type":"bool","longType":"bool","fullType":"bool","ismap":false,"isoneof":false,"oneofdecl":"","defaultValue":""},{"name":"reason","description":"If the account is not valid, a reason for why may be provided.","label":"optional","type":"string","longType":"string","fullType":"string","ismap":false,"isoneof":true,"oneofdecl":"_reason","defaultValue":""},{"name":"should_poll","description":"Whether or not the process should decide to continue polling for validitiy updates","label":"","type":"bool","longType":"bool","fullType":"bool","ismap":false,"isoneof":false,"oneofdecl":"","defaultValue":""},{"name":"used_record_count","description":"A count of the currently used records for the current billing period.\nThis may go over the allowed record count depending on when the record count is polled by the metric system.\n@deprecated","label":"","type":"uint64","longType":"uint64","fullType":"uint64","ismap":false,"isoneof":false,"oneofdecl":"","defaultValue":""},{"name":"allowed_record_count","description":"The allowed record count. It will be null if there is no limit.\n@deprecated","label":"optional","type":"uint64","longType":"uint64","fullType":"uint64","ismap":false,"isoneof":true,"oneofdecl":"_allowed_record_count","defaultValue":""},{"name":"account_status","description":"The current status of the account. Default is valid.","label":"","type":"AccountStatus","longType":"AccountStatus","fullType":"mgmt.v1alpha1.AccountStatus","ismap":false,"isoneof":false,"oneofdecl":"","defaultValue":"","typeLink":"/api/mgmt/v1alpha1/user_account.proto#accountstatus"},{"name":"trial_expires_at","description":"The time when the trial expires","label":"optional","type":"Timestamp","longType":"google.protobuf.Timestamp","fullType":"google.protobuf.Timestamp","ismap":false,"isoneof":true,"oneofdecl":"_trial_expires_at","defaultValue":""}]}} />


### `IsUserInAccountRequest`
Expand Down
12 changes: 12 additions & 0 deletions docs/protos/proto_docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -15766,6 +15766,18 @@
"isoneof": false,
"oneofdecl": "",
"defaultValue": ""
},
{
"name": "trial_expires_at",
"description": "The time when the trial expires",
"label": "optional",
"type": "Timestamp",
"longType": "google.protobuf.Timestamp",
"fullType": "google.protobuf.Timestamp",
"ismap": false,
"isoneof": true,
"oneofdecl": "_trial_expires_at",
"defaultValue": ""
}
]
},
Expand Down
74 changes: 74 additions & 0 deletions frontend/apps/web/components/site-header/AccountStatusHandler.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
'use client';
import { SystemAppConfig } from '@/app/config/app-config';
import { cn } from '@/libs/utils';
import { useQuery } from '@connectrpc/connect-query';
import { AccountStatus } from '@neosync/sdk';
import { isAccountStatusValid } from '@neosync/sdk/connectquery';
import { differenceInDays } from 'date-fns';
import { useAccount } from '../providers/account-provider';
import { Skeleton } from '../ui/skeleton';
import Upgrade from './Upgrade';
Expand All @@ -24,8 +27,28 @@ export function AccountStatusHandler(props: Props) {
return <Skeleton className="w-[100px] h-8" />;
}

const showTrialCountdown =
systemAppConfig.isNeosyncCloud &&
(data?.accountStatus == AccountStatus.ACCOUNT_TRIAL_ACTIVE ||
data?.accountStatus == AccountStatus.ACCOUNT_TRIAL_EXPIRED);

const trialEndDate = new Date(
data?.trialExpiresAt?.toDate() ?? Date.now()
).getTime();

const daysRemaining = Math.max(
differenceInDays(Math.max(trialEndDate), Date.now())
);

return (
<div className="flex flex-row items-center gap-2">
{showTrialCountdown && (
<TrialCountdown
isExpired={data?.accountStatus == AccountStatus.ACCOUNT_TRIAL_EXPIRED}
isAlmostExpired={daysRemaining <= 3}
daysRemaining={daysRemaining}
/>
)}
<Upgrade
calendlyLink={systemAppConfig.calendlyUpgradeLink}
isNeosyncCloud={systemAppConfig.isNeosyncCloud}
Expand All @@ -35,3 +58,54 @@ export function AccountStatusHandler(props: Props) {
</div>
);
}

interface TrialCountdownProps {
isExpired: boolean;
isAlmostExpired: boolean;
daysRemaining: number;
}

function TrialCountdown(props: TrialCountdownProps) {
const { isExpired, isAlmostExpired, daysRemaining } = props;

return (
<div
className={cn(
isExpired
? 'border-red-700'
: isAlmostExpired
? 'border-yellow-500'
: ' border-blue-400 dark:border-blue-700',
'border flex items-center gap-2 h-8 rounded-md px-2 py-1'
)}
>
<div className="relative flex items-center">
<div
className={cn(
isExpired
? 'bg-red-600'
: isAlmostExpired
? 'bg-yellow-600'
: ' border-blue-400 dark:border-blue-700',
'absolute animate-ping h-2.5 w-2.5 rounded-full bg-blue-400 opacity-75'
)}
/>
<div
className={cn(
isExpired
? 'bg-red-600'
: isAlmostExpired
? 'bg-yellow-600'
: 'bg-blue-700',
'relative h-2.5 w-2.5 rounded-full'
)}
/>
</div>
<div className="text-xs ">
{isExpired
? 'Trial Expired'
: `${daysRemaining} day${daysRemaining !== 1 ? 's' : ''} left in your trial`}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2032,6 +2032,13 @@ export class IsAccountStatusValidResponse extends Message<IsAccountStatusValidRe
*/
accountStatus = AccountStatus.REASON_UNSPECIFIED;

/**
* The time when the trial expires
*
* @generated from field: optional google.protobuf.Timestamp trial_expires_at = 7;
*/
trialExpiresAt?: Timestamp;

constructor(data?: PartialMessage<IsAccountStatusValidResponse>) {
super();
proto3.util.initPartial(data, this);
Expand All @@ -2046,6 +2053,7 @@ export class IsAccountStatusValidResponse extends Message<IsAccountStatusValidRe
{ no: 4, name: "used_record_count", kind: "scalar", T: 4 /* ScalarType.UINT64 */ },
{ no: 5, name: "allowed_record_count", kind: "scalar", T: 4 /* ScalarType.UINT64 */, opt: true },
{ no: 6, name: "account_status", kind: "enum", T: proto3.getEnumType(AccountStatus) },
{ no: 7, name: "trial_expires_at", kind: "message", T: Timestamp, opt: true },
]);

static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): IsAccountStatusValidResponse {
Expand Down
Loading

0 comments on commit 40640d8

Please sign in to comment.