planFeature.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import {Fragment} from 'react';
  2. import type {Organization} from 'sentry/types/organization';
  3. import {descopeFeatureName} from 'sentry/utils';
  4. import withSubscription from 'getsentry/components/withSubscription';
  5. import {useBillingConfig} from 'getsentry/hooks/useBillingConfig';
  6. import type {Plan, Subscription} from 'getsentry/types';
  7. import {isBizPlanFamily, isDeveloperPlan} from 'getsentry/utils/billing';
  8. type RenderProps = {
  9. /**
  10. * The plan that the user must upgrade to to use this feature.
  11. *
  12. * Null if there is no matching plan for the feature set. This can happen if
  13. * for example the features are only available on plans that are not
  14. * user-selectable or if the users current plan is on a special tier.
  15. */
  16. plan: Plan | null;
  17. /**
  18. * If the feature requires changing plan tiers, this will report the required
  19. * plan tier that is DIFFERENT from the users current subscription tier.
  20. */
  21. tierChange: string | null;
  22. };
  23. type Props = {
  24. children: (opts: RenderProps) => React.ReactNode;
  25. features: string[];
  26. organization: Organization;
  27. subscription: Subscription;
  28. };
  29. /**
  30. * Plan feature determines which plan a user must be on in order to access a
  31. * particular set of features.
  32. */
  33. function PlanFeature({subscription, features, organization, children}: Props) {
  34. const {data: billingConfig} = useBillingConfig({organization, subscription});
  35. if (!billingConfig) {
  36. return null;
  37. }
  38. const {billingInterval, contractInterval} = subscription;
  39. const billingIntervalFilter = (p: Plan) => p.billingInterval === billingInterval;
  40. // Plans may not have a contract interval.
  41. const contractIntervalFilter = (p: Plan) =>
  42. contractInterval === undefined || p.contractInterval === contractInterval;
  43. let plans = billingConfig.planList
  44. .filter(
  45. p =>
  46. p.userSelectable &&
  47. !isDeveloperPlan(p) &&
  48. // Only recommend business plans if the subscription is sponsored
  49. (subscription.isSponsored ? isBizPlanFamily(p) : true)
  50. )
  51. .sort((a, b) => a.price - b.price);
  52. // We try and keep the list of plans as close to the user current plan
  53. // configuration as we can, however some older plans (mm2) have
  54. // configurations not present in newer billing plans.
  55. //
  56. // As an example, am1 does NOT have plans where the contract interval is
  57. // different from the billing interval.
  58. //
  59. // Because of this we incrementally loosen the filters when we produce an
  60. // empty set of plans.
  61. function matchPlanConfiguration() {
  62. let filtered: Plan[] = [];
  63. filtered = plans.filter(billingIntervalFilter).filter(contractIntervalFilter);
  64. if (filtered.length > 0) {
  65. return filtered;
  66. }
  67. filtered = plans.filter(billingIntervalFilter);
  68. if (filtered.length > 0) {
  69. return filtered;
  70. }
  71. return plans;
  72. }
  73. plans = matchPlanConfiguration();
  74. // XXX: Enterprise plans are *not* user selectable, but should be included
  75. // in the list of plans. Unfortunately we don't distinguish between Trial /
  76. // Friends & Family / Enterprise, so we hardcode the name here.
  77. //
  78. // XXX(epurkhiser): We don't really have enterprise plans anymore, so maybe
  79. // we no longer need this.
  80. const enterprisePlans = billingConfig.planList
  81. .filter(billingIntervalFilter)
  82. .filter(p => p.id.includes('ent'));
  83. plans.push(...enterprisePlans);
  84. // If we're dealing with plans that are *not part of a tier* Then we can
  85. // assume special case that there is only one plan.
  86. if (billingConfig.id === null && plans.length === 0) {
  87. plans = billingConfig.planList;
  88. }
  89. // Locate the first plan that offers these features
  90. const requiredPlan = plans.find(plan =>
  91. features.map(descopeFeatureName).every(f => plan.features.includes(f))
  92. );
  93. const tierChange =
  94. requiredPlan !== undefined && subscription.planTier !== billingConfig.id
  95. ? billingConfig.id
  96. : null;
  97. return <Fragment>{children({plan: requiredPlan ?? null, tierChange})}</Fragment>;
  98. }
  99. export default withSubscription(PlanFeature, {noLoader: true});