Skip to content

Commit 83a9104

Browse files
committed
Better handle invalid stripe status (e.g.: unpaid, past_due)
1 parent c76d067 commit 83a9104

7 files changed

Lines changed: 138 additions & 69 deletions

File tree

landing/src/pages/AccountPage.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {
22
activePaidPlans,
33
allPlansObj,
4-
formatPrice,
4+
formatPriceAndInterval,
55
freeTrialDays,
6+
isPlanStatusValid,
67
} from '@brunolemos/devhub-core'
8+
import classNames from 'classnames'
79
import Link from 'next/link'
810
import qs from 'qs'
911
import React from 'react'
@@ -28,7 +30,7 @@ export default function AccountPage(_props: AccountPageProps) {
2830
const planInfo = authData.plan && allPlansObj[authData.plan.id]
2931

3032
const priceLabelForQuantity = authData.plan
31-
? formatPrice(
33+
? formatPriceAndInterval(
3234
authData.plan,
3335
authData.plan && { quantity: authData.plan.quantity },
3436
)
@@ -94,7 +96,13 @@ export default function AccountPage(_props: AccountPageProps) {
9496
<>
9597
<h2 className="mb-2 text-md sm:text-xl">
9698
Status:{' '}
97-
<span className="text-default">
99+
<span
100+
className={classNames(
101+
!isPlanStatusValid(authData.plan)
102+
? 'text-red'
103+
: 'text-default',
104+
)}
105+
>
98106
{authData.plan.status === 'trialing'
99107
? authData.plan.trialEndAt &&
100108
authData.plan.trialEndAt > new Date().toISOString()
@@ -239,7 +247,11 @@ export default function AccountPage(_props: AccountPageProps) {
239247
{ addQueryPrefix: true },
240248
)}`}
241249
>
242-
<a className="text-default">Update credit card</a>
250+
<a className="text-default">
251+
{!isPlanStatusValid(authData.plan)
252+
? '👉 Update credit card 👈'
253+
: 'Update credit card'}
254+
</a>
243255
</Link>
244256
)}
245257

packages/components/src/components/common/SidebarOrBottomBar.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getUserURLFromLogin,
77
GitHubIcon,
88
isItemRead,
9+
isPlanStatusValid,
910
ModalPayload,
1011
ThemeColors,
1112
} from '@devhub/core'
@@ -350,15 +351,17 @@ export const SidebarOrBottomBar = React.memo(
350351
(userPlan &&
351352
userPlan.status === 'active' &&
352353
userPlan.cancelAtPeriodEnd &&
353-
userPlan.cancelAt)
354+
userPlan.cancelAt) ||
355+
!isPlanStatusValid(userPlan)
354356
)
355357
}
356358
unreadIndicatorColor={
357359
(isPlanExpired && !(freePlan && !freePlan.trialPeriodDays)) ||
358360
(userPlan &&
359361
userPlan.status === 'active' &&
360362
userPlan.cancelAtPeriodEnd &&
361-
userPlan.cancelAt)
363+
userPlan.cancelAt) ||
364+
!isPlanStatusValid(userPlan)
362365
? 'red'
363366
: undefined
364367
}

packages/components/src/components/modals/SettingsModal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ export const SettingsModal = React.memo((props: SettingsModalProps) => {
8787
: userPlan.amount
8888
? ' (trial)'
8989
: ' trial'
90+
: !isPlanStatusValid(userPlan) &&
91+
userPlan &&
92+
userPlan.status
93+
? ` (${userPlan.status})`
9094
: ''
9195
} ↗`}</ThemedText>
9296
{!!(
@@ -98,8 +102,7 @@ export const SettingsModal = React.memo((props: SettingsModalProps) => {
98102
userPlan.cancelAt) ||
99103
(userPlan &&
100104
userPlan.status &&
101-
(!isPlanStatusValid(userPlan) ||
102-
userPlan.status === 'incomplete'))
105+
!isPlanStatusValid(userPlan))
103106
) && (
104107
<>
105108
<Spacer width={contentPadding / 2} />

packages/components/src/components/modals/partials/SubscribeForm.web.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ const SubscribeFormWithStripe = React.memo(
274274
dispatch(actions.updateUserData({ plan: data.subscribeToStripePlan }))
275275

276276
if (
277-
!isPlanStatusValid(data.subscribeToStripePlan) ||
277+
!isPlanStatusValid(data.subscribeToStripePlan) &&
278278
data.subscribeToStripePlan.status === 'incomplete'
279279
) {
280280
throw new Error('Please try a different credit card.')

packages/components/src/hooks/use-cards-props.tsx

Lines changed: 93 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getDateSmallText,
1010
getOwnerAndRepoFormattedFilter,
1111
getUsernamesFromFilter,
12+
isPlanStatusValid,
1213
} from '@devhub/core'
1314
import React, { useCallback, useMemo, useRef } from 'react'
1415
import { View } from 'react-native'
@@ -406,65 +407,99 @@ export function useCardsProps<ItemT extends EnhancedItem>({
406407
}
407408
}
408409

410+
if (isOverPlanColumnLimit && plan && plan.featureFlags.columnsLimit) {
411+
return {
412+
Component: () => (
413+
<EmptyCards
414+
columnId={column.id}
415+
emoji="rocket"
416+
errorButtonView={
417+
<Button
418+
analyticsLabel="unlock_more_columns_button"
419+
children="Unlock more columns"
420+
onPress={() => {
421+
const nextPlan = activePlans.find(
422+
p => p.featureFlags.columnsLimit > columnIndex + 1,
423+
)
424+
dispatch(
425+
actions.pushModal({
426+
name: 'PRICING',
427+
params: {
428+
highlightFeature: 'columnsLimit',
429+
initialSelectedPlanId: nextPlan && nextPlan.id,
430+
},
431+
}),
432+
)
433+
}}
434+
/>
435+
}
436+
errorMessage={`You have exceeded the limit of ${plan.featureFlags.columnsLimit} columns.`}
437+
errorTitle="Limit exceeded"
438+
fetchNextPage={undefined}
439+
loadState="error"
440+
refresh={undefined}
441+
/>
442+
),
443+
overlay: true,
444+
}
445+
}
446+
447+
if (!isPlanStatusValid(plan)) {
448+
return {
449+
Component: () => (
450+
<EmptyCards
451+
columnId={column.id}
452+
emoji="warning"
453+
errorButtonView={
454+
<ButtonLink
455+
analyticsLabel="select_a_plan_button"
456+
analyticsCategory="invalid_plan"
457+
children="Fix my subscription ↗"
458+
href={`${constants.DEVHUB_LINKS.ACCOUNT_PAGE}?appToken=${appToken}`}
459+
openOnNewTab
460+
/>
461+
}
462+
errorMessage={`Your current subscription status is ${
463+
plan && plan.status ? `"${plan.status}"` : 'invalid'
464+
}. This is usually fixed by updating your credit card.`}
465+
errorTitle="Action needed"
466+
fetchNextPage={undefined}
467+
loadState="error"
468+
refresh={undefined}
469+
/>
470+
),
471+
overlay: true,
472+
}
473+
}
474+
409475
if (isOverPlanColumnLimit) {
410476
return {
411-
Component: () =>
412-
plan && plan.featureFlags.columnsLimit ? (
413-
<EmptyCards
414-
columnId={column.id}
415-
emoji="rocket"
416-
errorButtonView={
417-
<Button
418-
analyticsLabel="unlock_more_columns_button"
419-
children="Unlock more columns"
420-
onPress={() => {
421-
const nextPlan = activePlans.find(
422-
p => p.featureFlags.columnsLimit > columnIndex + 1,
423-
)
424-
dispatch(
425-
actions.pushModal({
426-
name: 'PRICING',
427-
params: {
428-
highlightFeature: 'columnsLimit',
429-
initialSelectedPlanId: nextPlan && nextPlan.id,
430-
},
431-
}),
432-
)
433-
}}
434-
/>
435-
}
436-
errorMessage={`You have exceeded the limit of ${plan.featureFlags.columnsLimit} columns.`}
437-
errorTitle="Limit exceeded"
438-
fetchNextPage={undefined}
439-
loadState="error"
440-
refresh={undefined}
441-
/>
442-
) : (
443-
<EmptyCards
444-
columnId={column.id}
445-
emoji="lock"
446-
errorButtonView={
447-
<ButtonLink
448-
analyticsLabel="select_a_plan_button"
449-
analyticsCategory="invalid_plan"
450-
children={
451-
activePaidPlans.some(p => p.interval)
452-
? plan && plan.amount
453-
? 'Switch plan ↗'
454-
: 'Select a plan ↗'
455-
: 'See available options ↗'
456-
}
457-
href={`${constants.DEVHUB_LINKS.PRICING_PAGE}?appToken=${appToken}`}
458-
openOnNewTab
459-
/>
460-
}
461-
errorMessage="You need a paid plan to keep using DevHub."
462-
errorTitle="Upgrade to a paid plan"
463-
fetchNextPage={undefined}
464-
loadState="error"
465-
refresh={undefined}
466-
/>
467-
),
477+
Component: () => (
478+
<EmptyCards
479+
columnId={column.id}
480+
emoji="lock"
481+
errorButtonView={
482+
<ButtonLink
483+
analyticsLabel="select_a_plan_button"
484+
analyticsCategory="invalid_plan"
485+
children={
486+
activePaidPlans.some(p => p.interval)
487+
? plan && plan.amount
488+
? 'Switch plan ↗'
489+
: 'Select a plan ↗'
490+
: 'See available options ↗'
491+
}
492+
href={`${constants.DEVHUB_LINKS.PRICING_PAGE}?appToken=${appToken}`}
493+
openOnNewTab
494+
/>
495+
}
496+
errorMessage="You need a paid plan to keep using DevHub."
497+
errorTitle="Upgrade to a paid plan"
498+
fetchNextPage={undefined}
499+
loadState="error"
500+
refresh={undefined}
501+
/>
502+
),
468503
overlay: true,
469504
}
470505
}
@@ -478,6 +513,7 @@ export function useCardsProps<ItemT extends EnhancedItem>({
478513
isPlanExpired,
479514
appToken,
480515
!!(plan && plan.amount),
516+
isPlanStatusValid(plan),
481517
])
482518

483519
return useMemo(

packages/components/src/hooks/use-column.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ export function useColumn(columnId: string) {
3030

3131
const isOverPlanColumnLimit = !!(
3232
plan &&
33-
columnIndex + 1 > plan.featureFlags.columnsLimit &&
34-
(plan.featureFlags.columnsLimit || (freeTrialDays || isPlanExpired(plan)))
33+
(columnIndex + 1 > plan.featureFlags.columnsLimit &&
34+
(plan.featureFlags.columnsLimit ||
35+
(freeTrialDays || isPlanExpired(plan))))
3536
)
3637
const isOverMaxColumnLimit = !!(
3738
columnIndex >= 0 && columnIndex + 1 > constants.COLUMNS_LIMIT

packages/core/src/helpers/plans.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function getUserPlan(user: DatabaseUser): UserPlan {
7878
if (!plan) return defaultUserPlan
7979

8080
if (user.plan.amount) {
81-
if (!isPlanStatusValid(user.plan)) return defaultUserPlan
81+
if (!isPlanStatusHandled(user.plan)) return defaultUserPlan
8282
if (isPlanExpired(user.plan)) return defaultUserPlan
8383
}
8484

@@ -110,6 +110,20 @@ export function isPlanStatusValid(
110110
return !!(plan.status === 'active' || plan.status === 'trialing')
111111
}
112112

113+
export function isPlanStatusHandled(
114+
plan: Pick<UserPlan, 'status'> | undefined,
115+
): boolean {
116+
if (!plan) return false
117+
118+
return (
119+
isPlanStatusValid(plan) ||
120+
(plan.status === 'incomplete' ||
121+
plan.status === 'incomplete_expired' ||
122+
plan.status === 'past_due' ||
123+
plan.status === 'unpaid')
124+
)
125+
}
126+
113127
export function isPlanExpired(
114128
plan: Pick<UserPlan, 'amount' | 'status' | 'trialEndAt'> | undefined,
115129
): boolean {

0 commit comments

Comments
 (0)