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

Conversation

evisdrenova
Copy link
Contributor

@evisdrenova evisdrenova commented Oct 28, 2024

This feature adds a free trial countdown to the top nav menu to give users an idea of how many days they have left in their trial.

image

@evisdrenova evisdrenova added the enhancement New feature or request label Oct 28, 2024
Copy link

linear bot commented Oct 28, 2024

Copy link

vercel bot commented Oct 28, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
neosync-docs ⬜️ Ignored (Inspect) Visit Preview Oct 30, 2024 0:00am

Copy link

github-actions bot commented Oct 28, 2024

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedOct 30, 2024, 12:00 AM

Copy link

codecov bot commented Oct 28, 2024

Codecov Report

Attention: Patch coverage is 64.70588% with 6 lines in your changes missing coverage. Please review.

Project coverage is 39.04%. Comparing base (e1465e4) to head (184ac53).
Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
...ices/mgmt/v1alpha1/user-account-service/billing.go 64.70% 4 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2871      +/-   ##
==========================================
+ Coverage   29.58%   39.04%   +9.45%     
==========================================
  Files         303      303              
  Lines       35407    35434      +27     
==========================================
+ Hits        10476    13836    +3360     
+ Misses      23765    19924    -3841     
- Partials     1166     1674     +508     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@nickzelei nickzelei left a comment

Choose a reason for hiding this comment

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

This is pretty cool, left some comments regarding the handling of the logic.

@@ -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 timestamp of when the account
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
// The timestamp of when the account
// The timestamp of when the account was created.

Comment on lines 154 to 157
accountId, err := s.verifyUserInAccount(ctx, req.Msg.GetAccountId())
if err != nil {
return nil, err
}
Copy link
Member

Choose a reason for hiding this comment

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

This isn't really necessary here because the GetAccountStatus handles it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated


const TRIAL_DURATION_DAYS = 14;

// TODO: created_at is coming back as blank
Copy link
Member

Choose a reason for hiding this comment

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

Is this still valid?

// TODO: created_at is coming back as blank

function TrialCountdown(props: TrialCountdownProps) {
const { createdDate, isAccountStatusValidResp } = props;
Copy link
Member

Choose a reason for hiding this comment

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

As a heads up, we don't currently store our created at with a timestamp, so it comes back on the server as the date in UTC (which is fine because we handle all of the billing in UTC).
but you'll want to make sure all of the date handling here is done in UTC format.

isAccountStatusValidResp: IsAccountStatusValidResponse | undefined;
}

const TRIAL_DURATION_DAYS = 14;
Copy link
Member

Choose a reason for hiding this comment

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

Ideally we could do most or all of this on the server so we don't have this copied in two places.

Comment on lines 61 to 68
const now = Date.now();
const trialEndDate = new Date(
createdDate + TRIAL_DURATION_DAYS * 24 * 60 * 60 * 1000
);
const daysRemaining = Math.max(
0,
Math.ceil((trialEndDate.getTime() - now) / (1000 * 60 * 60 * 24))
);
Copy link
Member

Choose a reason for hiding this comment

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

Would be cleaner to use date-fns to do most of this for you so that it properly handles time changes like daylight savings, etc.

An example of something more functional:

import { differenceInDays, addDays, isAfter } from 'date-fns';


interface TrialStatus {
  isTrialExpired: boolean;
  isNearingEnd: boolean;
  daysRemaining: number;
  trialEndDate: Date;
}

export function checkTrialStatus(createdAt: Date): TrialStatus {
  const TRIAL_DURATION_DAYS = 14;
  const WARNING_THRESHOLD_DAYS = 3;
  
  const currentDate = new Date();
  const trialEndDate = addDays(createdAt, TRIAL_DURATION_DAYS);
  
  const daysRemaining = differenceInDays(trialEndDate, currentDate);
  
  return {
    isTrialExpired: isAfter(currentDate, trialEndDate),
    isNearingEnd: daysRemaining <= WARNING_THRESHOLD_DAYS && daysRemaining > 0,
    daysRemaining: Math.max(0, daysRemaining),
    trialEndDate
  };
}

And then to use it:

const trialStatus = checkTrialStatus(createdAt);

if (trialStatus.isTrialExpired) {
  console.log('Trial has expired');
} else if (trialStatus.isNearingEnd) {
  console.log(`Trial ending soon! ${trialStatus.daysRemaining} days remaining`);
} else {
  console.log(`${trialStatus.daysRemaining} days remaining in trial`);
}

Copy link
Member

Choose a reason for hiding this comment

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

I don't think this handles the last day (hours remaining) but you kind of get the point here.

Copy link
Member

@nickzelei nickzelei left a comment

Choose a reason for hiding this comment

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

Improvements. Still some logical issues with the handling of isExpired vs isAlmostExpired.
Would also be great to see the date-fns used here instead of manually calculating the difference.

Comment on lines 63 to 71
const daysRemaining = Math.max(
0,
Math.ceil((trialEndDate - now) / (1000 * 60 * 60 * 24))
);

const isExpired =
isAccountStatusValidResp?.accountStatus ==
AccountStatus.ACCOUNT_TRIAL_EXPIRED;
const isAlmostExpired = daysRemaining <= 3;
Copy link
Member

Choose a reason for hiding this comment

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

I still think this would be clearer if the date-fns were used here as it's going to handle more edge cases.

const daysRemaining = differenceInDays(trailEndDate, now);
const isAlmostExpired = daysRemaining <= 3


interface TrialCountdownProps {
trialEndDate: number;
isAccountStatusValidResp: IsAccountStatusValidResponse | undefined;
Copy link
Member

Choose a reason for hiding this comment

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

might be worth simplifying these props to just pass in an isExpired and do the calculation outside of this function. make it less smart.

AccountStatus.ACCOUNT_TRIAL_EXPIRED;
const isAlmostExpired = daysRemaining <= 3;

if (isExpired)
Copy link
Member

Choose a reason for hiding this comment

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

based on this logic it doesn't look like the isAlmostExpired will ever actually be used.
If isExpired is true, then isAlmostExpired will always be true..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

whoops meant to clean that up, that was leftover code

Copy link
Member

@nickzelei nickzelei left a comment

Choose a reason for hiding this comment

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

LGTM, minor thing regarding the math.max

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()));

@evisdrenova evisdrenova merged commit 40640d8 into main Oct 30, 2024
19 checks passed
@evisdrenova evisdrenova deleted the showFreeTrialRemainingTimeInHeader branch October 30, 2024 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants