Skip to content
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

NEOS-1579: add free trial timer to header #2871

Merged
merged 7 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
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
72 changes: 72 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,26 @@ 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 = differenceInDays(Math.max(trialEndDate), Date.now());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


return (
<div className="flex flex-row items-center gap-2">
{showTrialCountdown && (
<TrialCountdown
nickzelei marked this conversation as resolved.
Show resolved Hide resolved
isExpired={data?.accountStatus == AccountStatus.ACCOUNT_TRIAL_EXPIRED}
isAlmostExpired={daysRemaining <= 3}
daysRemaining={daysRemaining}
/>
)}
<Upgrade
calendlyLink={systemAppConfig.calendlyUpgradeLink}
isNeosyncCloud={systemAppConfig.isNeosyncCloud}
Expand All @@ -35,3 +56,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