Skip to content
This repository has been archived by the owner on Jun 26, 2022. It is now read-only.

Commit

Permalink
Payment Success Modal. (#36)
Browse files Browse the repository at this point in the history
* add in most of the wiring of the success modal

* add in modal data

* add in some tests

* turn off subscription feature

* add active product test and more test and fix some existing

* remove wait ref
  • Loading branch information
jcblw authored Jul 16, 2019
1 parent 1998168 commit 851f0c8
Show file tree
Hide file tree
Showing 13 changed files with 164 additions and 24 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Mujo",
"version": "1.1.3",
"version": "1.2.0",
"private": true,
"dependencies": {
"@babel/core": "7.4.3",
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"manifest_version": 2,
"version": "1.1.3",
"version": "1.2.0",
"short_name": "Mujo",
"name": "Mujō - Be mindful of your time",
"description": "Mujō is a extension that reminds you not to over work yourself.",
Expand Down
1 change: 1 addition & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ const App = () => {
) : null}
{upsellModal && (
<InfoModal
changeModal={setUpsellModal}
context={upsellModal}
isOpen={true}
onRequestClose={() => {
Expand Down
10 changes: 8 additions & 2 deletions src/components/info-modal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ import { Modal } from '../modal'
import { Description } from './description'
import { getModalData } from './modal-data'

export const InfoModal = ({ isOpen, onRequestClose, context }) => {
export const InfoModal = ({
isOpen,
onRequestClose,
context,
changeModal,
}) => {
const { highlight, backgroundSecondary } = useTheme()
const subDetails = useSubscription()
const { title, description, button } = getModalData(
context,
subDetails
subDetails,
{ changeModal }
)
return (
<Modal
Expand Down
3 changes: 3 additions & 0 deletions src/components/info-modal/modal-data.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import {
MAX_BREAKTIMER_MODAL,
SUB_DETAILS_MODAL,
SUB_SUCCESS_MODAL,
} from '../../constants'
import { identity } from '../../lib/functional'
import { maxBreaktimers } from './max-breaktimers'
import { subscriptionDetails } from './subscription-details'
import { subscriptionSuccess } from './subscription-success'

const modals = {
[MAX_BREAKTIMER_MODAL]: maxBreaktimers,
[SUB_DETAILS_MODAL]: subscriptionDetails,
[SUB_SUCCESS_MODAL]: subscriptionSuccess,
}

export const getModalData = (context, subDetails) => {
Expand Down
21 changes: 15 additions & 6 deletions src/components/info-modal/subscription-details.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import { CURRENT_SUB_SKU } from '../../constants'
import { first } from '../../lib/functional'
import { CURRENT_SUB_SKU, SUB_SUCCESS_MODAL } from '../../constants'
import { first, noop } from '../../lib/functional'
import { PurchaseError } from './purchase-error'

// const mockProduct = {
Expand All @@ -25,15 +25,22 @@ import { PurchaseError } from './purchase-error'
// ],
// }

export const subscriptionDetails = (context, subDetails) => {
export const subscriptionDetails = (
context,
subDetails,
{ changeModal }
) => {
const sku = context.sku || CURRENT_SUB_SKU
const callback = context.callback || noop
const product = subDetails.getProduct(sku)

if (!product) {
// TODO: logging monitoring
return {
title: 'Unable to subscribe at this time',
description: ['Please contact support'],
description: [
'Please contact support at jacoblowe2.0@gmail.com',
],
}
}

Expand Down Expand Up @@ -68,8 +75,10 @@ export const subscriptionDetails = (context, subDetails) => {
children: `Subscribe at ${formatter.format(
price.valueMicros / 1000 / 1000
)}/mo`,
onClick: () => {
subDetails.buy(sku)
onClick: async () => {
await subDetails.buy(sku)
changeModal({ name: SUB_SUCCESS_MODAL })
callback() // set initial action
},
},
}
Expand Down
73 changes: 73 additions & 0 deletions src/components/info-modal/subscription-details.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { subscriptionDetails } from './subscription-details'

const stampSubDetails = isSub => ({
user: { isSubscriber: isSub, products: [] },
products: [],
getProduct: jest.fn(),
buy: jest.fn(),
})

test('subscriptionDetails should return error when no product is found', () => {
const context = {}
const subDetails = stampSubDetails()
const callback = jest.fn()
expect(
subscriptionDetails(context, subDetails, callback).title
).toMatch('Unable to subscribe')
})

test('subscriptionDetails should return detail when there is a product', () => {
const context = {}
const subDetails = stampSubDetails()
const callback = jest.fn()
subDetails.getProduct.mockReturnValue({
sku: 'foo',
prices: [
{
valueMicros: '990000',
currencyCode: 'USD',
regionCode: 'US',
},
],
localeData: [
{
title: 'Foo',
description: 'Foo bar baz',
languageCode: 'all',
},
],
})
expect(
subscriptionDetails(context, subDetails, callback).button.children
).toMatch('$0.99/mo') // correct pricing format
})

test('subscriptionDetails should call buy when button is clicked', async () => {
const context = { sku: 'foo', callback: jest.fn() }
const subDetails = stampSubDetails()
const changeModal = jest.fn()
subDetails.getProduct.mockReturnValue({
sku: 'foo',
prices: [
{
valueMicros: '990000',
currencyCode: 'USD',
regionCode: 'US',
},
],
localeData: [
{
title: 'Foo',
description: 'Foo bar baz',
languageCode: 'all',
},
],
})
subDetails.buy.mockResolvedValue(true)
const options = { changeModal }
const modal = subscriptionDetails(context, subDetails, options)
await modal.button.onClick()
expect(subDetails.buy).toBeCalledWith('foo')
expect(context.callback).toBeCalled()
expect(changeModal).toBeCalled()
})
7 changes: 7 additions & 0 deletions src/components/info-modal/subscription-success.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const subscriptionSuccess = (context, subDetails) => ({
title: 'Thanks for purchasing a subscription',
description: [
'Your support is greatly appreciated!',
'Please contact jacoblowe2.0@gmail.com with any comment or questions.',
],
})
5 changes: 5 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export const SUBSCRIBE_FEATURE = false
// Upsell modals
export const MAX_BREAKTIMER_MODAL = 'breakTimerMax'
export const SUB_DETAILS_MODAL = 'subscriptionDetails'
export const SUB_SUCCESS_MODAL = 'subSuccess'

// Product States
export const ACTIVE_PRODUCT = 'ACTIVE'
export const CANCELLED_PRODUCT = 'CANCELLED'

// OPTIONAL permissions requested
export const SCREEN_TIME_PERMISSIONS = {
Expand Down
4 changes: 2 additions & 2 deletions src/e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
SITE_TIME_KEY,
APP_READY_KEY,
} from './constants'
import { wait } from './lib/async-helpers'

const TEST_TIMEOUT = 10000 // extend test timeout sinces its E2E

Expand All @@ -26,6 +25,7 @@ beforeAll(async () => {
width: 1280,
height: 800,
},
slowMo: 100,
})
})

Expand Down Expand Up @@ -110,7 +110,7 @@ test(
SITE_TIME_KEY,
screenTimeMock
)
await wait(500)
// await wait(500)
const el = await page.$('[data-testid="graph"]')
expect(el).not.toBe(null)
// const screenshot = await page.screenshot()
Expand Down
1 change: 1 addition & 0 deletions src/hooks/use-extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export const useExtension = () => {
setUpsellModal({
name: SUB_DETAILS_MODAL,
sku: CURRENT_SUB_SKU,
callback: () => setBreakTimer(url, time, enabled),
}),
})
return
Expand Down
22 changes: 15 additions & 7 deletions src/hooks/use-subscription.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { useState, useEffect, useCallback } from 'react'
import { ACTIVE_PRODUCT } from '../constants'
import { compose, first } from '../lib/functional'
import {
buy as buyProduct,
getPurchases,
getProducts,
} from '../lib/payment'

export const activeProducts = products =>
products.filter(product => product.state === ACTIVE_PRODUCT)

export const userFactory = (products = []) => ({
isSubscribed: !!products.length,
isSubscribed: !!activeProducts(products).length,
products,
has: product => products.includes(product),
})
Expand All @@ -27,6 +31,11 @@ export const useSubscription = () => {
const [user, setUser] = useState(userFactory())
const [purchaseError, setPurchaseError] = useState(null)

const getProduct = useCallback(
sku => first(products.filter(product => product.sku === sku)),
[products]
)

const buy = useCallback(
async sku => {
setPurchaseError(null) // reset error
Expand All @@ -35,15 +44,14 @@ export const useSubscription = () => {
} catch (e) {
return setPurchaseError(e)
}
// eager update user
setUser(
Object.assign({}, user, { products: [getProduct(sku)] })
)
// rehydrate user
return hydrate({ setProducts, setUser })
},
[setPurchaseError, setUser, setProducts]
)

const getProduct = useCallback(
sku => first(products.filter(product => product.sku === sku)),
[products]
[user, getProduct]
)

useEffect(() => {
Expand Down
37 changes: 32 additions & 5 deletions src/hooks/use-subscription.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { renderHook, act } from '@testing-library/react-hooks'
import { useSubscription } from './use-subscription'
import { ACTIVE_PRODUCT, CANCELLED_PRODUCT } from '../constants'
import { useSubscription, activeProducts } from './use-subscription'

jest.mock('../lib/payment')
/* eslint-disable-next-line import-order-alphabetical/order */
Expand All @@ -11,8 +12,16 @@ beforeEach(() => {
buy.mockReset()
})

test('activeProduct should filter to only active products', () => {
const products = [
{ state: ACTIVE_PRODUCT },
{ state: CANCELLED_PRODUCT },
]
expect(activeProducts(products)).toHaveLength(1)
})

test('useSubscription should initialize products and purchases', async () => {
getPurchases.mockResolvedValue(['foo'])
getPurchases.mockResolvedValue([{ state: ACTIVE_PRODUCT }])
getProducts.mockResolvedValue(['foo', 'bar'])
const { result, waitForNextUpdate } = renderHook(() =>
useSubscription()
Expand All @@ -21,10 +30,24 @@ test('useSubscription should initialize products and purchases', async () => {
await waitForNextUpdate()
await waitForNextUpdate()
expect(result.current.user.isSubscribed).toBe(true)
expect(result.current.user.products).toEqual(['foo'])
expect(result.current.user.products).toEqual([
{ state: ACTIVE_PRODUCT },
])
expect(result.current.products).toEqual(['foo', 'bar'])
})

test('useSubscription should be unsubbed if not active product', async () => {
getPurchases.mockResolvedValue([{ state: CANCELLED_PRODUCT }])
getProducts.mockResolvedValue(['foo', 'bar'])
const { result, waitForNextUpdate } = renderHook(() =>
useSubscription()
)
// Two updates [product, user]
await waitForNextUpdate()
await waitForNextUpdate()
expect(result.current.user.isSubscribed).toBe(false)
})

test('useSubscription should get purchases after calling buy', async () => {
getPurchases.mockResolvedValue([])
getProducts.mockResolvedValue(['foo', 'bar'])
Expand All @@ -38,15 +61,19 @@ test('useSubscription should get purchases after calling buy', async () => {
expect(result.current.user.isSubscribed).toBe(false)
expect(result.current.user.products).toEqual([])
// mock purchase again with purchased value
getPurchases.mockReset().mockResolvedValue(['foo'])
getPurchases
.mockReset()
.mockResolvedValue([{ state: ACTIVE_PRODUCT }])
act(() => {
result.current.buy('foo')
})
// Update calls
await waitForNextUpdate()
await waitForNextUpdate()
expect(result.current.user.isSubscribed).toBe(true)
expect(result.current.user.products).toEqual(['foo'])
expect(result.current.user.products).toEqual([
{ state: ACTIVE_PRODUCT },
])
expect(result.current.products).toEqual(['foo', 'bar'])
})

Expand Down

0 comments on commit 851f0c8

Please sign in to comment.