-
-
Notifications
You must be signed in to change notification settings - Fork 167
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
Remove free tier threshold defaults, make Plans page respect custom free tiers #4531
Changes from 11 commits
9bc1c33
781a9fc
04c5244
c34c78e
d0a2347
113a93b
4d413e4
f75e884
cd8b028
e03ecad
0fcbd6b
43b9793
722b2bb
7225590
2521a5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,9 @@ import Button from 'js/components/common/button'; | |
import classnames from 'classnames'; | ||
import LoadingSpinner from 'js/components/common/loadingSpinner'; | ||
import {notify} from 'js/utils'; | ||
import {BaseProduct} from "js/account/subscriptionStore"; | ||
import {BaseProduct} from 'js/account/subscriptionStore'; | ||
import EnvStore, {FreeTierThresholds, FreeTierDisplay} from 'js/envStore'; | ||
import envStore from 'js/envStore'; | ||
|
||
interface PlanState { | ||
subscribedProduct: null | BaseSubscription; | ||
|
@@ -44,6 +46,11 @@ interface DataUpdates { | |
prodData?: any; | ||
} | ||
|
||
interface FreeTierOverride extends FreeTierThresholds{ | ||
name: string | null; | ||
[key: `feature_list_${number}`]: string | null; | ||
} | ||
|
||
const initialState = { | ||
subscribedProduct: null, | ||
intervalFilter: 'year', | ||
|
@@ -100,8 +107,28 @@ export default function Plan() { | |
[state.products, state.organization, state.subscribedProduct] | ||
); | ||
|
||
const hasManageableStatus = useCallback((subscription: BaseSubscription) => | ||
activeSubscriptionStatuses.includes(subscription.status), []); | ||
const freeTierOverride = useMemo((): FreeTierOverride | null => { | ||
if (envStore.isReady) { | ||
const thresholds = envStore.data.free_tier_thresholds as FreeTierThresholds; | ||
const display = envStore.data.free_tier_display as FreeTierDisplay; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need the as part. Without FreeTierDisplay it will error because it seems to think it could also be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might either define it as The usage of "as" overrides type checking, making it less useful. It would be better to review how the nullish state should actually be presented. |
||
const featureList : {[key: string]: string | null} = {}; | ||
display.feature_list.forEach((feature, key) => { | ||
featureList[`feature_list_${key+1}`] = feature; | ||
}); | ||
return { | ||
name: display.name, | ||
...thresholds, | ||
...featureList, | ||
}; | ||
} | ||
return null; | ||
}, [envStore.isReady]); | ||
|
||
const hasManageableStatus = useCallback( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a useCallback because you want it to be a function that accepts various subscriptions right? Just checking my own understanding, I don't think there is a change to make. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep! |
||
(subscription: BaseSubscription) => | ||
activeSubscriptionStatuses.includes(subscription.status), | ||
[] | ||
); | ||
|
||
const hasActiveSubscription = useMemo(() => { | ||
if (state.subscribedProduct) { | ||
|
@@ -113,9 +140,7 @@ export default function Plan() { | |
}, [state.subscribedProduct]); | ||
|
||
useMemo(() => { | ||
if ( | ||
state.subscribedProduct?.length > 0 | ||
) { | ||
if (state.subscribedProduct?.length > 0) { | ||
const subscribedFilter = | ||
state.subscribedProduct?.[0].items[0].price.recurring?.interval; | ||
if (!hasManageableStatus(state.subscribedProduct)) { | ||
|
@@ -207,7 +232,10 @@ export default function Plan() { | |
const filterAmount = state.products.map((product: Product) => { | ||
const filteredPrices = product.prices.filter((price: BasePrice) => { | ||
const interval = price.recurring?.interval; | ||
return interval === state.intervalFilter && product.metadata.product_type === 'plan'; | ||
return ( | ||
interval === state.intervalFilter && | ||
product.metadata.product_type === 'plan' | ||
); | ||
}); | ||
|
||
return { | ||
|
@@ -216,8 +244,12 @@ export default function Plan() { | |
}; | ||
}); | ||
|
||
return filterAmount.filter((product: Product) => product.prices) | ||
.sort((priceA: Price, priceB: Price) => priceA.prices.unit_amount > priceB.prices.unit_amount); | ||
return filterAmount | ||
.filter((product: Product) => product.prices) | ||
.sort( | ||
(priceA: Price, priceB: Price) => | ||
priceA.prices.unit_amount > priceB.prices.unit_amount | ||
); | ||
} | ||
return []; | ||
}, [state.products, state.intervalFilter]); | ||
|
@@ -244,9 +276,10 @@ export default function Plan() { | |
const subscriptions = getSubscriptionsForProductId(product.id); | ||
|
||
if (subscriptions.length > 0) { | ||
return subscriptions.some((subscription: BaseSubscription) => | ||
subscription.items[0].price.id === product.prices.id && | ||
hasManageableStatus(subscription) | ||
return subscriptions.some( | ||
(subscription: BaseSubscription) => | ||
subscription.items[0].price.id === product.prices.id && | ||
hasManageableStatus(subscription) | ||
); | ||
} | ||
return false; | ||
|
@@ -262,7 +295,7 @@ export default function Plan() { | |
} | ||
|
||
return subscriptions.some((subscription: BaseSubscription) => | ||
hasManageableStatus(subscription) | ||
hasManageableStatus(subscription) | ||
); | ||
}, | ||
[state.subscribedProduct] | ||
|
@@ -347,6 +380,17 @@ export default function Plan() { | |
return expandBool; | ||
}; | ||
|
||
const getFeatureMetadata = (price: Price, featureItem: string) => { | ||
if ( | ||
price.prices.unit_amount === 0 && | ||
freeTierOverride && | ||
freeTierOverride.hasOwnProperty(featureItem) | ||
) { | ||
return freeTierOverride[featureItem as keyof FreeTierOverride]; | ||
} | ||
return price.prices.metadata?.[featureItem] || price.metadata[featureItem]; | ||
}; | ||
|
||
useEffect(() => { | ||
hasMetaFeatures(); | ||
}, [state.products]); | ||
|
@@ -453,7 +497,11 @@ export default function Plan() { | |
[styles.planContainer]: true, | ||
})} | ||
> | ||
<h1 className={styles.priceName}> {price.name} </h1> | ||
<h1 className={styles.priceName}> | ||
{price.prices?.unit_amount | ||
? price.name | ||
: freeTierOverride?.name || price.name} | ||
</h1> | ||
<div className={styles.priceTitle}> | ||
{!price.prices?.unit_amount | ||
? t('Free') | ||
|
@@ -476,8 +524,7 @@ export default function Plan() { | |
} | ||
/> | ||
</div> | ||
{price.prices.metadata?.[featureItem] || | ||
price.metadata[featureItem]} | ||
{getFeatureMetadata(price, featureItem)} | ||
</li> | ||
) | ||
)} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import json | ||
|
||
from constance import config | ||
from django.db import migrations | ||
|
||
|
||
def reset_free_tier_thresholds(apps, schema_editor): | ||
# The constance defaults for FREE_TIER_THRESHOLDS changed, so we set existing config to the new default value | ||
thresholds = { | ||
'storage': None, | ||
'data': None, | ||
'transcription_minutes': None, | ||
'translation_chars': None, | ||
} | ||
setattr(config, 'FREE_TIER_THRESHOLDS', json.dumps(thresholds)) | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('kpi', '0050_add_indexes_to_import_and_export_tasks'), | ||
] | ||
|
||
operations = [ | ||
migrations.RunPython( | ||
reset_free_tier_thresholds, | ||
migrations.RunPython.noop, | ||
) | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EnvStore doesn't appear to be used. If this unused import is important for some reason, it should be documented.