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

Commit

Permalink
Fixes to purchase flow. (#37)
Browse files Browse the repository at this point in the history
* revise beta deploy and subscrription  hooks

* add in new beta icon

* update eager update to include subscriber status.

* add in a couple more fixes

* update test and snapshots
  • Loading branch information
jcblw committed Jul 24, 2019
1 parent 851f0c8 commit 5b66a35
Show file tree
Hide file tree
Showing 13 changed files with 108 additions and 60 deletions.
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"name": "Mujo",
"version": "1.2.0",
"version": "1.2.4",
"private": true,
"homepage": "./",
"dependencies": {
"@babel/core": "7.4.3",
"@emotion/cache": "^10.0.14",
Expand Down Expand Up @@ -74,15 +75,17 @@
"scripts": {
"serve": "serve",
"start": "node scripts/watch.js",
"build": "PUBLIC_URL=./ node scripts/build.js && npm run append:sw && npm run append:sm",
"build:beta": "BETA='true' node scripts/build.js && npm run build:post && npm run append:beta",
"build:post": "npm run append:sw && npm run append:sm",
"build": "PUBLIC_URL=./ node scripts/build.js && npm run build:post",
"append:sw": "cra-append-sw --mode replace --skip-compile ./public/service-worker.js",
"append:sm": "node scripts/modify-source-maps.js",
"test": "npm run build && node scripts/test.js --env=jsdom",
"fmt": "prettier '{src,public}/**/*.js' --write",
"lint": "eslint '{src,public}/**/*.js'",
"dist": "npm run build && ./scripts/dist.sh",
"dist:beta": "npm run build && npm run beta && ./scripts/dist.sh",
"beta": "node scripts/beta-decorate.js"
"dist:beta": "npm run build:beta && ./scripts/dist.sh",
"append:beta": "node scripts/beta-decorate.js"
},
"browserslist": [
"last 3 Chrome versions"
Expand Down
Binary file modified public/favicon-beta.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.2.0",
"version": "1.2.1",
"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
3 changes: 2 additions & 1 deletion scripts/beta-decorate.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const readJSON = async dir => JSON.parse(await read(dir))
const serialize = json => JSON.stringify(json, null, '\t')

const decorateBETA = async () => {
const secondsInWeek = `${Math.floor(+new Date() / 1000)}`.slice(-4)
const manifest = await readJSON(MANIFEST_PATH)
const betaManifest = Object.assign({}, manifest, {
name: beta(manifest.name),
Expand All @@ -26,7 +27,7 @@ const decorateBETA = async () => {
128: BETA_ICON,
512: BETA_ICON,
},
version: `${manifest.version}.${`${+new Date()}`.slice(0, 4)}`,
version: `${manifest.version}.${secondsInWeek}`,
version_name: `${manifest.version} BETA`,
})
return write(MANIFEST_PATH, serialize(betaManifest))
Expand Down
7 changes: 7 additions & 0 deletions src/__snapshots__/app.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should match the snapshot 1`] = `
<div
className="css-1dl0z63"
/>
`;
6 changes: 6 additions & 0 deletions src/app.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import React from 'react'
import ReactDOM from 'react-dom'
import ReactTestRenderer from 'react-test-renderer'
import App from './app'

it('renders without crashing', () => {
const div = document.createElement('div')
ReactDOM.render(<App />, div)
})

it('should match the snapshot', async () => {
const tree = ReactTestRenderer.create(<App />).toJSON()
expect(tree).toMatchSnapshot()
})
23 changes: 23 additions & 0 deletions src/components/info-modal/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,29 @@ exports[`InfoModal should match snapshot 1`] = `
>
You have set the max number of break timers.
</p>
<p
className="css-120ftwk"
color="color"
>
To get access to unlimited break timers and add a break timer for foo subscribe to Mujō.
</p>
</div>
<div
backgroundColor="gravel"
className="css-1yqdfgl"
>
<button
backgroundColor="saltBox"
className="css-15foh21"
color="white"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
outlineColor="gravel"
position="relative"
>
Subscription Details
</button>
</div>
</div>
`;
4 changes: 2 additions & 2 deletions src/components/info-modal/modal-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const modals = {
[SUB_SUCCESS_MODAL]: subscriptionSuccess,
}

export const getModalData = (context, subDetails) => {
export const getModalData = (context, subDetails, options) => {
const modalData = modals[context.name] || identity
return modalData(context, subDetails)
return modalData(context, subDetails, options)
}
2 changes: 1 addition & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const SET_STORAGE = 'SET_STORAGE'
// Feature Flags
export const SCREEN_TIME_FEATURE = true
export const BREAK_TIMER_FEATURE = true
export const SUBSCRIBE_FEATURE = false
export const SUBSCRIBE_FEATURE = true

// Upsell modals
export const MAX_BREAKTIMER_MODAL = 'breakTimerMax'
Expand Down
38 changes: 27 additions & 11 deletions src/hooks/use-subscription.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { useState, useEffect, useCallback } from 'react'
import React, {
useState,
useEffect,
useCallback,
useContext,
} from 'react'
import { ACTIVE_PRODUCT } from '../constants'
import { compose, first } from '../lib/functional'
import {
Expand All @@ -25,7 +30,8 @@ export const hydrate = async ({ setProducts, setUser }) => {
setProducts(await getProducts())
}

export const useSubscription = () => {
const context = React.createContext({ user: userFactory([]) })
export const SubscriptionProvider = props => {
const [isInitialized, setIsInitialized] = useState(false)
const [products, setProducts] = useState([])
const [user, setUser] = useState(userFactory())
Expand All @@ -42,16 +48,18 @@ export const useSubscription = () => {
try {
await buyProduct(sku)
} catch (e) {
return setPurchaseError(e)
setPurchaseError(e)
return
}
// eager update user
setUser(
Object.assign({}, user, { products: [getProduct(sku)] })
Object.assign({}, user, {
products: [getProduct(sku)],
isSubscribed: true,
})
)
// rehydrate user
return hydrate({ setProducts, setUser })
},
[user, getProduct]
[getProduct, user]
)

useEffect(() => {
Expand All @@ -61,11 +69,19 @@ export const useSubscription = () => {
}
}, [setProducts, setUser, isInitialized, setIsInitialized])

return {
user,
products,
const { Provider } = context

const value = {
buy,
purchaseError,
getProduct,
isInitialized,
products,
purchaseError,
user,
}
// eslint-disable-next-line no-underscore-dangle
window.__MUJO_SUBSCRIPTION__ = value
return <Provider {...props} value={value} />
}

export const useSubscription = () => useContext(context)
52 changes: 16 additions & 36 deletions src/hooks/use-subscription.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { renderHook, act } from '@testing-library/react-hooks'
import { ACTIVE_PRODUCT, CANCELLED_PRODUCT } from '../constants'
import { useSubscription, activeProducts } from './use-subscription'
import {
useSubscription,
activeProducts,
SubscriptionProvider,
} from './use-subscription'

jest.mock('../lib/payment')
/* eslint-disable-next-line import-order-alphabetical/order */
const { getPurchases, getProducts, buy } = require('../lib/payment')

const hookOptions = { wrapper: SubscriptionProvider }

beforeEach(() => {
getPurchases.mockReset()
getProducts.mockReset()
Expand All @@ -23,8 +29,9 @@ test('activeProduct should filter to only active products', () => {
test('useSubscription should initialize products and purchases', async () => {
getPurchases.mockResolvedValue([{ state: ACTIVE_PRODUCT }])
getProducts.mockResolvedValue(['foo', 'bar'])
const { result, waitForNextUpdate } = renderHook(() =>
useSubscription()
const { result, waitForNextUpdate } = renderHook(
() => useSubscription(),
hookOptions
)
// Two updates [product, user]
await waitForNextUpdate()
Expand All @@ -39,50 +46,23 @@ test('useSubscription should initialize products and purchases', async () => {
test('useSubscription should be unsubbed if not active product', async () => {
getPurchases.mockResolvedValue([{ state: CANCELLED_PRODUCT }])
getProducts.mockResolvedValue(['foo', 'bar'])
const { result, waitForNextUpdate } = renderHook(() =>
useSubscription()
const { result, waitForNextUpdate } = renderHook(
() => useSubscription(),
hookOptions
)
// 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'])
buy.mockResolvedValue(true)
const { result, waitForNextUpdate } = renderHook(() =>
useSubscription()
)
// Init calls
await waitForNextUpdate()
await waitForNextUpdate()
expect(result.current.user.isSubscribed).toBe(false)
expect(result.current.user.products).toEqual([])
// mock purchase again with purchased value
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([
{ state: ACTIVE_PRODUCT },
])
expect(result.current.products).toEqual(['foo', 'bar'])
})

test('useSubscription a failure to buy should set purchaseError', async () => {
getPurchases.mockResolvedValue([])
getProducts.mockResolvedValue([])
buy.mockRejectedValue(new Error('fail'))
const { result, waitForNextUpdate } = renderHook(() =>
useSubscription()
const { result, waitForNextUpdate } = renderHook(
() => useSubscription(),
hookOptions
)
// Init calls
await waitForNextUpdate()
Expand Down
9 changes: 6 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useColorScheme } from 'use-color-scheme'
import App from './app'
import { ErrorBox } from './components/error-box'
import { Font } from './components/fonts'
import { SubscriptionProvider } from './hooks/use-subscription'
import { ColorThemeProvider } from './hooks/use-theme'

import './lib/tracker'
Expand All @@ -13,9 +14,11 @@ const NewHomePage = () => {
return (
<ErrorBox>
<Font />
<ColorThemeProvider value={scheme}>
<App />
</ColorThemeProvider>
<SubscriptionProvider>
<ColorThemeProvider value={scheme}>
<App />
</ColorThemeProvider>
</SubscriptionProvider>
</ErrorBox>
)
}
Expand Down
11 changes: 10 additions & 1 deletion src/lib/aggregation.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FOURTY_FIVE_MINUTES } from '../constants'
import { shortURL } from './url'
import { set } from './util'

Expand Down Expand Up @@ -30,7 +31,15 @@ export const toSiteInfo = (times, timers) =>
Object.keys(times).reduce((accum, url) => {
set(accum, url, accum[url] || {})
set(accum[url], 'time', times[url])
set(accum[url], 'breakTimer', timers[url] || {})
set(
accum[url],
'breakTimer',
timers[url] || {
enabled: false,
time: FOURTY_FIVE_MINUTES,
url,
}
)
return accum
}, {})

Expand Down

0 comments on commit 5b66a35

Please sign in to comment.