[CLD-6536] Adjustments to cloud annual renewal announcement bar (#25927)
* Adjustments to cloud annual renewal announcement bar * Add exclusion for trials * Add more exceptions for when on trial during renewal period * Add support for simulated_current_time_ms * A few more changes to allow us to test this post-merge * Fix tests, pipeline * Final fix around emails --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -111,6 +111,10 @@ func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if model.GetServiceEnvironment() != model.ServiceEnvironmentTest {
|
||||||
|
subscription.SimulatedCurrentTimeMs = nil
|
||||||
|
}
|
||||||
|
|
||||||
if !c.App.Config().FeatureFlags.CloudAnnualRenewals {
|
if !c.App.Config().FeatureFlags.CloudAnnualRenewals {
|
||||||
subscription.WillRenew = ""
|
subscription.WillRenew = ""
|
||||||
subscription.CancelAt = nil
|
subscription.CancelAt = nil
|
||||||
|
|||||||
@@ -285,6 +285,10 @@ func (a *App) DoSubscriptionRenewalCheck() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if subscription.IsFreeTrial == "true" {
|
||||||
|
return // Don't send renewal emails for free trials
|
||||||
|
}
|
||||||
|
|
||||||
sysVar, err := a.Srv().Store().System().GetByName(model.CloudRenewalEmail)
|
sysVar, err := a.Srv().Store().System().GetByName(model.CloudRenewalEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// We only care about the error if it wasn't a not found error
|
// We only care about the error if it wasn't a not found error
|
||||||
@@ -318,13 +322,13 @@ func (a *App) DoSubscriptionRenewalCheck() {
|
|||||||
|
|
||||||
// Only send the email if within the period and it's not already been sent
|
// Only send the email if within the period and it's not already been sent
|
||||||
// This allows the email to send on day 59 if for whatever reason it was unable to on day 60
|
// This allows the email to send on day 59 if for whatever reason it was unable to on day 60
|
||||||
if daysToExpiration <= 60 && daysToExpiration > 30 && prevSentEmail != 60 {
|
if daysToExpiration <= 60 && daysToExpiration > 30 && prevSentEmail != 60 && !(prevSentEmail < 60) {
|
||||||
emailFunc = a.Srv().EmailService.SendCloudRenewalEmail60
|
emailFunc = a.Srv().EmailService.SendCloudRenewalEmail60
|
||||||
prevSentEmail = 60
|
prevSentEmail = 60
|
||||||
} else if daysToExpiration <= 30 && daysToExpiration > 7 && prevSentEmail != 30 {
|
} else if daysToExpiration <= 30 && daysToExpiration > 7 && prevSentEmail != 30 && !(prevSentEmail < 30) {
|
||||||
emailFunc = a.Srv().EmailService.SendCloudRenewalEmail30
|
emailFunc = a.Srv().EmailService.SendCloudRenewalEmail30
|
||||||
prevSentEmail = 30
|
prevSentEmail = 30
|
||||||
} else if daysToExpiration <= 7 && daysToExpiration > 3 && prevSentEmail != 7 {
|
} else if daysToExpiration <= 7 && daysToExpiration >= 0 && prevSentEmail != 7 {
|
||||||
emailFunc = a.Srv().EmailService.SendCloudRenewalEmail7
|
emailFunc = a.Srv().EmailService.SendCloudRenewalEmail7
|
||||||
prevSentEmail = 7
|
prevSentEmail = 7
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -185,17 +183,16 @@ type Subscription struct {
|
|||||||
BillingType string `json:"billing_type"`
|
BillingType string `json:"billing_type"`
|
||||||
CancelAt *int64 `json:"cancel_at"`
|
CancelAt *int64 `json:"cancel_at"`
|
||||||
WillRenew string `json:"will_renew"`
|
WillRenew string `json:"will_renew"`
|
||||||
|
SimulatedCurrentTimeMs *int64 `json:"simulated_current_time_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscription) DaysToExpiration() int64 {
|
func (s *Subscription) DaysToExpiration() int64 {
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
// Allows us to base the current time off of an environment variable for testing purposes
|
|
||||||
if GetServiceEnvironment() == ServiceEnvironmentTest {
|
if GetServiceEnvironment() == ServiceEnvironmentTest {
|
||||||
if currTime, set := os.LookupEnv("CLOUD_MOCK_CURRENT_TIME"); set {
|
// In the test environment we have test clocks. A test clock is a ms timestamp
|
||||||
timeInt, err := strconv.ParseInt(currTime, 10, 64)
|
// If it's not nil, we use it as the current time in all calculations
|
||||||
if err == nil {
|
if s.SimulatedCurrentTimeMs != nil {
|
||||||
now = time.Unix(timeInt, 0).UnixMilli()
|
now = *s.SimulatedCurrentTimeMs
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
daysToExpiry := (s.EndAt - now) / (1000 * 60 * 60 * 24)
|
daysToExpiry := (s.EndAt - now) / (1000 * 60 * 60 * 24)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import useGetSubscription from 'components/common/hooks/useGetSubscription';
|
|||||||
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||||
|
|
||||||
import {daysToExpiration} from 'utils/cloud_utils';
|
import {daysToCancellation, daysToExpiration} from 'utils/cloud_utils';
|
||||||
|
|
||||||
export const creditCardExpiredBanner = (setShowCreditCardBanner: (value: boolean) => void) => {
|
export const creditCardExpiredBanner = (setShowCreditCardBanner: (value: boolean) => void) => {
|
||||||
return (
|
return (
|
||||||
@@ -68,8 +68,8 @@ export const CloudAnnualRenewalBanner = () => {
|
|||||||
if (!subscription || !subscription.cancel_at || (subscription.will_renew === 'true' && !subscription.delinquent_since)) {
|
if (!subscription || !subscription.cancel_at || (subscription.will_renew === 'true' && !subscription.delinquent_since)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const daysUntilExpiration = daysToExpiration(subscription?.end_at);
|
const daysUntilExpiration = daysToExpiration(subscription);
|
||||||
const daysUntilCancelation = daysToExpiration(subscription?.cancel_at);
|
const daysUntilCancelation = daysToCancellation(subscription);
|
||||||
const renewButton = (
|
const renewButton = (
|
||||||
<button
|
<button
|
||||||
className='btn btn-primary'
|
className='btn btn-primary'
|
||||||
@@ -96,6 +96,11 @@ export const CloudAnnualRenewalBanner = () => {
|
|||||||
message: <></>,
|
message: <></>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If outside the 60 day window or on a trial, don't show this banner.
|
||||||
|
if (daysUntilExpiration > 60 || subscription.is_free_trial === 'true') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (daysUntilExpiration <= 7) {
|
if (daysUntilExpiration <= 7) {
|
||||||
alertBannerProps.mode = 'danger';
|
alertBannerProps.mode = 'danger';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const CloudAnnualRenewalAnnouncementBar = () => {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return daysToExpiration(subscription.end_at);
|
return daysToExpiration(subscription);
|
||||||
}, [subscription]);
|
}, [subscription]);
|
||||||
|
|
||||||
const handleDismiss = (banner: string) => {
|
const handleDismiss = (banner: string) => {
|
||||||
@@ -89,7 +89,7 @@ const CloudAnnualRenewalAnnouncementBar = () => {
|
|||||||
...defaultProps,
|
...defaultProps,
|
||||||
type: '',
|
type: '',
|
||||||
};
|
};
|
||||||
if (between(daysUntilExpiration, 31, 60) && !hasDismissed60DayBanner) {
|
if (between(daysUntilExpiration, 31, 60)) {
|
||||||
if (hasDismissed60DayBanner) {
|
if (hasDismissed60DayBanner) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -112,6 +112,7 @@ const CloudAnnualRenewalAnnouncementBar = () => {
|
|||||||
handleClose: () => handleDismiss(CloudBanners.ANNUAL_RENEWAL_30_DAY),
|
handleClose: () => handleDismiss(CloudBanners.ANNUAL_RENEWAL_30_DAY),
|
||||||
};
|
};
|
||||||
} else if (between(daysUntilExpiration, 0, 7) && !isDelinquencySubscription()) {
|
} else if (between(daysUntilExpiration, 0, 7) && !isDelinquencySubscription()) {
|
||||||
|
// This banner is not dismissable
|
||||||
bannerProps = {
|
bannerProps = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
message: (<>{formatMessage({id: 'cloud_annual_renewal.banner.message.7', defaultMessage: 'Your annual subscription expires in {days} days. Failure to renew will result in your workspace being deleted.'}, {days: daysUntilExpiration})}</>),
|
message: (<>{formatMessage({id: 'cloud_annual_renewal.banner.message.7', defaultMessage: 'Your annual subscription expires in {days} days. Failure to renew will result in your workspace being deleted.'}, {days: daysUntilExpiration})}</>),
|
||||||
@@ -119,13 +120,16 @@ const CloudAnnualRenewalAnnouncementBar = () => {
|
|||||||
type: AnnouncementBarTypes.CRITICAL,
|
type: AnnouncementBarTypes.CRITICAL,
|
||||||
showCloseButton: false,
|
showCloseButton: false,
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
// If none of the above, return null, so that a blank announcement bar isn't visible
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <AnnouncementBar {...bannerProps}/>;
|
return <AnnouncementBar {...bannerProps}/>;
|
||||||
}, [daysUntilExpiration, hasDismissed60DayBanner, hasDismissed30DayBanner]);
|
}, [daysUntilExpiration, hasDismissed60DayBanner, hasDismissed30DayBanner]);
|
||||||
|
|
||||||
// Delinquent subscriptions will have a cancel_at time, but the banner is handled separately
|
// Delinquent subscriptions will have a cancel_at time, but the banner is handled separately
|
||||||
if (!cloudAnnualRenewalsEnabled || !subscription || !subscription.cancel_at || subscription.will_renew === 'true' || isDelinquencySubscription() || !isAdmin || daysUntilExpiration > 60) {
|
if (!cloudAnnualRenewalsEnabled || !subscription || !subscription.cancel_at || subscription.is_free_trial === 'true' || subscription.will_renew === 'true' || isDelinquencySubscription() || !isAdmin || daysUntilExpiration > 60) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
const subscription = state.entities.cloud.subscription;
|
const subscription = state.entities.cloud.subscription;
|
||||||
|
|
||||||
const isDelinquencyModal = Boolean(state.entities.cloud.subscription?.delinquent_since);
|
const isDelinquencyModal = Boolean(state.entities.cloud.subscription?.delinquent_since);
|
||||||
const isRenewalModal = daysToExpiration(Number(state.entities.cloud.subscription?.end_at)) <= 60 && !isDelinquencyModal && getConfig(state).FeatureFlags?.CloudAnnualRenewals;
|
const isRenewalModal = daysToExpiration(state.entities.cloud.subscription) <= 60 && state.entities.cloud.subscription?.is_free_trial === 'false' && !isDelinquencyModal && getConfig(state).FeatureFlags?.CloudAnnualRenewals;
|
||||||
const products = state.entities.cloud!.products;
|
const products = state.entities.cloud!.products;
|
||||||
const yearlyProducts = findOnlyYearlyProducts(products || {});
|
const yearlyProducts = findOnlyYearlyProducts(products || {});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import type {CloudCustomer, InvoiceLineItem} from '@mattermost/types/cloud';
|
import type {CloudCustomer, InvoiceLineItem, Subscription} from '@mattermost/types/cloud';
|
||||||
|
|
||||||
import {trackEvent} from 'actions/telemetry_actions';
|
import {trackEvent} from 'actions/telemetry_actions';
|
||||||
|
|
||||||
@@ -47,9 +47,22 @@ export function openExternalPricingLink() {
|
|||||||
|
|
||||||
export const FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS = 30;
|
export const FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS = 30;
|
||||||
|
|
||||||
export function daysToExpiration(expirationDate: number): number {
|
export function daysUntil(end?: number, simulatedCurrentTimeMs?: number) {
|
||||||
const now = new Date();
|
let now = new Date();
|
||||||
const expiration = new Date(expirationDate);
|
|
||||||
|
if (simulatedCurrentTimeMs) {
|
||||||
|
now = new Date(simulatedCurrentTimeMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiration = new Date(end || 0);
|
||||||
const diff = expiration.getTime() - now.getTime();
|
const diff = expiration.getTime() - now.getTime();
|
||||||
return Math.ceil(diff / (1000 * 3600 * 24));
|
return Math.ceil(diff / (1000 * 3600 * 24));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function daysToExpiration(subscription?: Subscription): number {
|
||||||
|
return daysUntil(subscription?.end_at, subscription?.simulated_current_time_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function daysToCancellation(subscription?: Subscription): number {
|
||||||
|
return daysUntil(subscription?.cancel_at, subscription?.simulated_current_time_ms);
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export type Subscription = {
|
|||||||
billing_type?: string;
|
billing_type?: string;
|
||||||
cancel_at?: number;
|
cancel_at?: number;
|
||||||
will_renew?: string;
|
will_renew?: string;
|
||||||
|
simulated_current_time_ms?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Product = {
|
export type Product = {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user