diff --git a/src/authentication/upgrade-modal/index.tsx b/src/authentication/upgrade-modal/index.tsx index f78e2a15af..5951d3d2e4 100644 --- a/src/authentication/upgrade-modal/index.tsx +++ b/src/authentication/upgrade-modal/index.tsx @@ -13,6 +13,7 @@ import Icon from '../../../external/@worldbrain/memex-common/ts/common-ui/compon import LoadingIndicator from '@worldbrain/memex-common/lib/common-ui/components/loading-indicator' import { PrimaryAction } from '@worldbrain/memex-common/lib/common-ui/components/PrimaryAction' import { DEFAULT_POWERUP_LIMITS } from '@worldbrain/memex-common/lib/subscriptions/constants' +import { PremiumPlans } from '@worldbrain/memex-common/lib/subscriptions/availablePowerups' export default class UpgradeModal extends UIElement< PromptTemplatesDependencies, @@ -31,6 +32,50 @@ export default class UpgradeModal extends UIElement< async componentWillUnmount(): Promise {} + renderConfirmUpgradeOverlay = (powerup: PremiumPlans) => { + let confirmTitle = 'Confirm Upgrade' + let confirmSubTitle = 'with obligation to pay' + + if ( + this.state.confirmPowerups === 'AIpowerupBasic' || + this.state.confirmPowerups === 'bookmarksPowerUpBasic' + ) { + confirmTitle = 'Confirm Downgrade' + confirmSubTitle = 'Ends at the current billig period' + } + + return ( + + + {confirmTitle} + {confirmSubTitle} + + + { + this.processEvent('processCheckoutOpen', { + plan: powerup, + }) + }} + /> + { + this.processEvent('setPowerUpConfirmation', { + selected: null, + }) + }} + /> + + + ) + } + renderAIPowerUpsOptionsList = () => { if ( this.state.checkoutLoading === 'running' || @@ -97,10 +142,9 @@ export default class UpgradeModal extends UIElement< this.state.activatedPowerUps['AIpowerupOwnKey'] === true ) { - this.processEvent( - 'processCheckoutOpen', - 'AIpowerupBasic', - ) + this.processEvent('processCheckoutOpen', { + plan: 'AIpowerupBasic', + }) } }} activated={ @@ -128,10 +172,9 @@ export default class UpgradeModal extends UIElement< this.state.activatedPowerUps && this.state.activatedPowerUps['AIpowerup'] === false ) { - this.processEvent( - 'processCheckoutOpen', - 'AIpowerup', - ) + this.processEvent('processCheckoutOpen', { + plan: 'AIpowerup', + }) } }} activated={ @@ -176,16 +219,19 @@ export default class UpgradeModal extends UIElement< this.state.activatedPowerUps['AIpowerupOwnKey'] === false ) { - this.processEvent( - 'processCheckoutOpen', - 'AIpowerupOwnKey', - ) + this.processEvent('processCheckoutOpen', { + plan: 'AIpowerupOwnKey', + }) } }} activated={ this.state.activatedPowerUps && this.state.activatedPowerUps['AIpowerupOwnKey'] === true } + disabled={ + this.state.activatedPowerUps && + this.state.activatedPowerUps['AIpowerup'] === true + } > @@ -276,10 +322,9 @@ export default class UpgradeModal extends UIElement< this.state.activatedPowerUps.bookmarksPowerUp === true ) { - this.processEvent( - 'processCheckoutOpen', - 'bookmarksPowerUpBasic', - ) + this.processEvent('processCheckoutOpen', { + plan: 'bookmarksPowerUpBasic', + }) } }} activated={ @@ -287,19 +332,21 @@ export default class UpgradeModal extends UIElement< this.state.activatedPowerUps.bookmarksPowerUp === false } > - - + <> + + + {' '} + {powerUp.powerUps.basic.title} + + + {powerUp.powerUps.basic.subTitle} + + + {' '} - {powerUp.powerUps.basic.title} - - - {powerUp.powerUps.basic.subTitle} - - - - {' '} - {powerUp.powerUps.basic.pricing} - + {powerUp.powerUps.basic.pricing} + + { @@ -308,10 +355,9 @@ export default class UpgradeModal extends UIElement< this.state.activatedPowerUps.bookmarksPowerUp === false ) { - this.processEvent( - 'processCheckoutOpen', - 'bookmarksPowerUp', - ) + this.processEvent('processCheckoutOpen', { + plan: 'bookmarksPowerUp', + }) } }} activated={ @@ -389,7 +435,9 @@ export default class UpgradeModal extends UIElement< { - this.processEvent('processCheckoutOpen', 'lifetime') + this.processEvent('processCheckoutOpen', { + plan: 'lifetime', + }) }} activated={ this.state.activatedPowerUps && @@ -433,71 +481,79 @@ export default class UpgradeModal extends UIElement< const upgradeContent = ( - - Powerups - {Powerups.map((powerUp) => ( - { - this.processEvent( - 'changeModalType', - powerUp.id as PowerUpModalVersion, - ) - }} - selected={powerUp.id === this.state.powerUpType} - > - - {powerUp.title} - - ))} - - {this.state.authLoadState === 'running' ? ( - - ) : ( - this.state.activatedPowerUps != null && ( - + + Powerups + {Powerups.map((powerUp) => ( + { - const isStaging = - process.env.REACT_APP_FIREBASE_PROJECT_ID?.includes( - 'staging', - ) || - process.env.NODE_ENV === - 'development' - window.open( - isStaging - ? `https://billing.stripe.com/p/login/test_bIY036ggb10LeqYeUU?prefilled_email=${this.state.userEmail}` - : `https://billing.stripe.com/p/login/8wM015dIp6uPdb2288?prefilled_email=${this.state.userEmail}`, - '_blank', + this.processEvent( + 'changeModalType', + powerUp.id as PowerUpModalVersion, ) }} - width="100%" - /> - ) - )} - - + selected={ + powerUp.id === this.state.powerUpType + } + > + + {powerUp.title} + + ))} + + {this.state.authLoadState === 'running' ? ( + + ) : ( + this.state.activatedPowerUps != null && ( + { + const isStaging = + process.env.REACT_APP_FIREBASE_PROJECT_ID?.includes( + 'staging', + ) || + process.env.NODE_ENV === + 'development' + window.open( + isStaging + ? `https://billing.stripe.com/p/login/test_bIY036ggb10LeqYeUU?prefilled_email=${this.state.userEmail}` + : `https://billing.stripe.com/p/login/8wM015dIp6uPdb2288?prefilled_email=${this.state.userEmail}`, + '_blank', + ) + }} + width="100%" + /> + ) + )} + + - - {modalToShow}{' '} - {this.props.componentVariant !== 'AccountPage' && ( - - - 60-day money back guarantee - - )} - + + {modalToShow}{' '} + {this.props.componentVariant !== 'AccountPage' && ( + + + 60-day money back guarantee + + )} + + + )} ) @@ -618,6 +674,7 @@ const OverlayContainer = styled.div` height: fit-content; min-height: 470px; box-sizing: border-box; + border-radius: 20px; ` const PowerUpOptions = styled.div` @@ -634,6 +691,7 @@ const PowerUpOptions = styled.div` const PowerUpItem = styled.div<{ activated?: boolean + disabled?: boolean }>` display: flex; align-items: center; @@ -668,6 +726,30 @@ const PowerUpItem = styled.div<{ justify-content: center; } `} + ${(props) => + props.disabled && + css` + border: initial; + position: relative; + opacity: 0.8; + &::after { + content: 'Included in Pro'; + background-color: ${(props) => props.theme.colors.prime1}; + border-radius: 0 5px 0 5px; + position: absolute; + top: 0px; + right: 0px; + height: 20px; + font-size: 12px; + padding: 0 5px; + display: flex; + align-items: center; + justify-content: center; + } + &:hover { + background: unset; + } + `} ` const PowerUpTitleBox = styled.div` @@ -926,3 +1008,47 @@ const LifetimePlanTermsListItem = styled.div` align-items: center; color: ${(props) => props.theme.colors.greyScale6}; ` + +const ConfirmOverlay = styled.div` + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + background: ${(props) => props.theme.colors.greyScale1}; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + grid-gap: 30px; + border-radius: inherit; +` + +const ConfirmText = styled.div` + color: ${(props) => props.theme.colors.greyScale7}; + font-size: 20px; + font-weight: 700; + text-align: center; + justify-content: center; +` +const ConfirmSubText = styled.div` + color: ${(props) => props.theme.colors.greyScale6}; + font-size: 18px; + font-weight: 400; + text-align: center; + justify-content: center; +` + +const ButtonBox = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + grid-gap: 10px; +` + +const ConfirmContainer = styled.div` + display: flex; + flex-direction: column; + grid-gap: 10px; +` diff --git a/src/authentication/upgrade-modal/logic.ts b/src/authentication/upgrade-modal/logic.ts index 69156d567c..e0b0561624 100644 --- a/src/authentication/upgrade-modal/logic.ts +++ b/src/authentication/upgrade-modal/logic.ts @@ -55,6 +55,7 @@ export default class PromptTemplatesLogic extends UILogic< activatedPowerUps: null, authLoadState: 'running', userEmail: null, + confirmPowerups: null, } } @@ -63,6 +64,13 @@ export default class PromptTemplatesLogic extends UILogic< powerUpType: { $set: event }, }) } + setPowerUpConfirmation: EventHandler<'setPowerUpConfirmation'> = async ({ + event, + }) => { + this.emitMutation({ + confirmPowerups: { $set: event.selected }, + }) + } processCheckoutOpen: EventHandler<'processCheckoutOpen'> = async ({ event, @@ -71,6 +79,18 @@ export default class PromptTemplatesLogic extends UILogic< if (previousState.activatedPowerUps.lifetime === true) { return } + + if (event.plan !== previousState.confirmPowerups) { + this.emitMutation({ + confirmPowerups: { $set: event.plan }, + }) + return + } else { + this.emitMutation({ + confirmPowerups: { $set: null }, + }) + } + this.emitMutation({ checkoutLoading: { $set: 'running' }, }) @@ -89,28 +109,28 @@ export default class PromptTemplatesLogic extends UILogic< let newSelection: PremiumPlans[] = currentlySelected - if (event === 'AIpowerupBasic') { + if (event.plan === 'AIpowerupBasic') { newSelection = newSelection.filter( (key) => key !== 'AIpowerup' && key !== 'AIpowerupOwnKey', ) - } else if (event === 'bookmarksPowerUpBasic') { + } else if (event.plan === 'bookmarksPowerUpBasic') { newSelection = newSelection.filter( (key) => key !== 'bookmarksPowerUp', ) - } else if (event === 'AIpowerup') { + } else if (event.plan === 'AIpowerup') { newSelection = newSelection.filter( (key) => key !== 'AIpowerupOwnKey', ) - newSelection.push(event) - } else if (event === 'AIpowerupOwnKey') { + newSelection.push(event.plan) + } else if (event.plan === 'AIpowerupOwnKey') { newSelection = newSelection.filter((key) => key !== 'AIpowerup') - newSelection.push(event) - } else if (event === 'lifetime') { + newSelection.push(event.plan) + } else if (event.plan === 'lifetime') { newSelection = ['lifetime'] billingPeriod = null doNotOpen = false } else { - newSelection.push(event) + newSelection.push(event.plan) } const upgradeResponse = await this.dependencies.createCheckOutLink({ diff --git a/src/authentication/upgrade-modal/types.ts b/src/authentication/upgrade-modal/types.ts index 941a6242c8..deb6c6726c 100644 --- a/src/authentication/upgrade-modal/types.ts +++ b/src/authentication/upgrade-modal/types.ts @@ -25,14 +25,16 @@ export interface PromptTemplatesState { componentVariant: 'Modal' | 'PricingList' | 'AccountPage' | 'OnboardingStep' powerUpType: PowerUpModalVersion activatedPowerUps?: Record + confirmPowerups?: PremiumPlans authLoadState: UITaskState userEmail?: string } export type PromptTemplatesEvent = UIEvent<{ changeModalType: PowerUpModalVersion - processCheckoutOpen: PremiumPlans + processCheckoutOpen: { plan: PremiumPlans } toggleBillingPeriod: 'monthly' | 'yearly' + setPowerUpConfirmation: { selected?: PremiumPlans } }> export type PowerUpModalVersion = 'Bookmarks' | 'AI' | 'AIownKey' | 'lifetime' diff --git a/src/content-scripts/content_script/global.ts b/src/content-scripts/content_script/global.ts index 9c516fa18c..5dd81a6144 100644 --- a/src/content-scripts/content_script/global.ts +++ b/src/content-scripts/content_script/global.ts @@ -1637,7 +1637,7 @@ export async function main( if (email) { let hasSubscriptionUpdated = false let retries = 0 - const maxRetries = 30 + const maxRetries = 60 while (!hasSubscriptionUpdated && retries < maxRetries) { const subscriptionBefore = await browser.storage.local.get( COUNTER_STORAGE_KEY, @@ -1651,28 +1651,26 @@ export async function main( DEFAULT_COUNTER_STORAGE_VALUE.pU await sleepPromise(1000) - await runInBackground< + const status = await runInBackground< InPageUIInterface<'caller'> >().checkStripePlan(email) - const subscriptionAfter = await browser.storage.local.get( - COUNTER_STORAGE_KEY, - ) - const subscriptionDataAfter = - subscriptionAfter[COUNTER_STORAGE_KEY] + let subscriptionHasChanged = false - const subscriptionsAfter = - subscriptionDataAfter?.pU ?? - DEFAULT_COUNTER_STORAGE_VALUE.pU + const statusKeys = Object.keys(status) - let keyChanged = false - for (const key in subscriptionsBefore) { - if (subscriptionsBefore[key] !== subscriptionsAfter[key]) { - keyChanged = true + for (let key of statusKeys) { + if (status[key] === subscriptionsBefore[key]) continue + if (status[key] !== subscriptionsBefore[key]) { + subscriptionHasChanged = true break } + if (status[key] && !subscriptionsBefore[key]) { + continue + } } - if (keyChanged) { + + if (subscriptionHasChanged) { hasSubscriptionUpdated = true if (h2Element) { h2Element.textContent = @@ -1690,6 +1688,31 @@ export async function main( } } + function deepEqual(obj1, obj2) { + if (obj1 === obj2) return true + + if ( + typeof obj1 !== 'object' || + obj1 === null || + typeof obj2 !== 'object' || + obj2 === null + ) { + return false + } + + const keys1 = Object.keys(obj1) + + for (let key of keys1) { + if (obj1[key] === obj2[key]) continue + if (obj1[key] !== obj2[key]) return true + if (obj1[key] && !obj2[key]) { + continue + } + } + + return false + } + // Function to track when to show the nudges let tabOpenedTime = Date.now() let activeTabTime = 0 diff --git a/src/in-page-ui/background/index.ts b/src/in-page-ui/background/index.ts index 84dc686b7c..2f78198632 100644 --- a/src/in-page-ui/background/index.ts +++ b/src/in-page-ui/background/index.ts @@ -113,7 +113,7 @@ export class InPageUIBackground { await this.options.tabsAPI.create({ url: OVERVIEW_URL }) } async checkStripePlan(email: string) { - await checkStripePlan(email, this.options.browserAPIs) + return await checkStripePlan(email, this.options.browserAPIs) } async getCurrentTabURL() { const tabs = await this.options.tabsAPI.query({ diff --git a/src/in-page-ui/background/types.ts b/src/in-page-ui/background/types.ts index eeff8033cf..950e24845b 100644 --- a/src/in-page-ui/background/types.ts +++ b/src/in-page-ui/background/types.ts @@ -1,9 +1,10 @@ +import { PremiumPlans } from '@worldbrain/memex-common/lib/subscriptions/availablePowerups' import { RemoteFunction, RemoteFunctionRole } from 'src/util/webextensionRPC' export interface InPageUIInterface { showSidebar: RemoteFunction openDashboard: RemoteFunction getCurrentTabURL: () => Promise - checkStripePlan: (email: string) => Promise + checkStripePlan: (email: string) => Promise> updateContextMenuEntries: RemoteFunction }