@artsy/dismissible 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.autorc +3 -0
- package/.circleci/config.yml +42 -0
- package/.eslintrc.js +31 -0
- package/.husky/post-merge +4 -0
- package/.husky/pre-commit +4 -0
- package/.husky/pre-push +4 -0
- package/.lintstagedrc.json +6 -0
- package/.prettierrc +6 -0
- package/.vscode/settings.json +23 -0
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/index.js +7 -0
- package/jest.config.js +7 -0
- package/package.json +48 -0
- package/src/DismissibleContext.tsx +238 -0
- package/src/__tests__/DismissibleContext.jest.tsx +261 -0
- package/src/index.ts +4 -0
- package/tsconfig.build.json +6 -0
- package/tsconfig.json +27 -0
package/.autorc
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
version: 2.1
|
|
2
|
+
|
|
3
|
+
orbs:
|
|
4
|
+
yarn: artsy/yarn@6.2.0
|
|
5
|
+
auto: artsy/auto@2.1.0
|
|
6
|
+
|
|
7
|
+
workflows:
|
|
8
|
+
build_and_verify:
|
|
9
|
+
jobs:
|
|
10
|
+
- yarn/workflow-queue
|
|
11
|
+
- yarn/update-cache:
|
|
12
|
+
requires:
|
|
13
|
+
- yarn/workflow-queue
|
|
14
|
+
- yarn/lint:
|
|
15
|
+
requires:
|
|
16
|
+
- yarn/workflow-queue
|
|
17
|
+
- yarn/type-check:
|
|
18
|
+
requires:
|
|
19
|
+
- yarn/workflow-queue
|
|
20
|
+
- yarn/test:
|
|
21
|
+
requires:
|
|
22
|
+
- yarn/workflow-queue
|
|
23
|
+
- auto/publish-canary:
|
|
24
|
+
context: npm-deploy
|
|
25
|
+
filters:
|
|
26
|
+
branches:
|
|
27
|
+
ignore: main
|
|
28
|
+
requires:
|
|
29
|
+
- yarn/test
|
|
30
|
+
- yarn/lint
|
|
31
|
+
- yarn/type-check
|
|
32
|
+
- yarn/update-cache
|
|
33
|
+
- auto/publish:
|
|
34
|
+
context: npm-deploy
|
|
35
|
+
filters:
|
|
36
|
+
branches:
|
|
37
|
+
only: main
|
|
38
|
+
requires:
|
|
39
|
+
- yarn/test
|
|
40
|
+
- yarn/lint
|
|
41
|
+
- yarn/type-check
|
|
42
|
+
- yarn/update-cache
|
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
module.exports = {
|
|
3
|
+
env: {
|
|
4
|
+
es6: true,
|
|
5
|
+
browser: true,
|
|
6
|
+
jest: true,
|
|
7
|
+
node: true,
|
|
8
|
+
},
|
|
9
|
+
extends: [
|
|
10
|
+
"eslint:recommended",
|
|
11
|
+
"plugin:@typescript-eslint/recommended",
|
|
12
|
+
"prettier",
|
|
13
|
+
],
|
|
14
|
+
parser: "@typescript-eslint/parser",
|
|
15
|
+
parserOptions: {
|
|
16
|
+
ecmaFeatures: {
|
|
17
|
+
jsx: true,
|
|
18
|
+
},
|
|
19
|
+
ecmaVersion: 12,
|
|
20
|
+
sourceType: "module",
|
|
21
|
+
},
|
|
22
|
+
plugins: ["@typescript-eslint", "prettier"],
|
|
23
|
+
rules: {
|
|
24
|
+
"@typescript-eslint/ban-ts-comment": 0,
|
|
25
|
+
"@typescript-eslint/ban-ts-ignore": 0,
|
|
26
|
+
"@typescript-eslint/explicit-function-return-type": 0,
|
|
27
|
+
"@typescript-eslint/explicit-module-boundary-types": 0,
|
|
28
|
+
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
29
|
+
"prettier/prettier": 0,
|
|
30
|
+
},
|
|
31
|
+
}
|
package/.husky/pre-push
ADDED
package/.prettierrc
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files.exclude": {
|
|
3
|
+
".git/": true,
|
|
4
|
+
"node_modules/": true,
|
|
5
|
+
"dist/": true
|
|
6
|
+
},
|
|
7
|
+
"search.exclude": {
|
|
8
|
+
"**/node_modules": true,
|
|
9
|
+
"**/dist": true
|
|
10
|
+
},
|
|
11
|
+
"editor.codeActionsOnSave": {
|
|
12
|
+
"source.fixAll.eslint": "explicit"
|
|
13
|
+
},
|
|
14
|
+
"eslint.validate": ["javascript", "typescript"],
|
|
15
|
+
"editor.rulers": [80],
|
|
16
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
17
|
+
"editor.tabSize": 2,
|
|
18
|
+
"editor.formatOnSave": true,
|
|
19
|
+
"[json]": {
|
|
20
|
+
"editor.formatOnSave": false
|
|
21
|
+
},
|
|
22
|
+
"typescript.tsdk": "./node_modules/typescript/lib"
|
|
23
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Artsy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @artsy/dismissible
|
|
2
|
+
|
|
3
|
+
A package that stores dismissible key/value entries in localStorage, which can be used in apps that require progressive onboarding and the like.
|
|
4
|
+
|
|
5
|
+
## Use
|
|
6
|
+
|
|
7
|
+
TODO
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
yarn test
|
|
13
|
+
yarn type-check
|
|
14
|
+
yarn lint
|
|
15
|
+
yarn compile
|
|
16
|
+
yarn watch
|
|
17
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useDismissibleContext = exports.DismissibleProvider = void 0;
|
|
4
|
+
var DismissibleContext_1 = require("./DismissibleContext");
|
|
5
|
+
Object.defineProperty(exports, "DismissibleProvider", { enumerable: true, get: function () { return DismissibleContext_1.DismissibleProvider; } });
|
|
6
|
+
Object.defineProperty(exports, "useDismissibleContext", { enumerable: true, get: function () { return DismissibleContext_1.useDismissibleContext; } });
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@artsy/dismissible",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"clean": "rm -rf dist",
|
|
9
|
+
"compile": "tsc --project tsconfig.build.json",
|
|
10
|
+
"lint": "eslint --cache --ext ts,tsx --ignore-pattern 'dist'",
|
|
11
|
+
"prepare": "husky install",
|
|
12
|
+
"prepublish": "yarn compile",
|
|
13
|
+
"release": "auto shipit",
|
|
14
|
+
"test": "jest",
|
|
15
|
+
"type-check": "tsc --noEmit --pretty",
|
|
16
|
+
"watch": "tsc -p . --watch"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@artsy/auto-config": "^1.2.0",
|
|
20
|
+
"@testing-library/react-hooks": "^8.0.1",
|
|
21
|
+
"@types/jest": "^29.1.2",
|
|
22
|
+
"@types/lodash.uniqby": "^4.7.9",
|
|
23
|
+
"@types/react": "^18.2.45",
|
|
24
|
+
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
|
25
|
+
"@typescript-eslint/parser": "^5.40.0",
|
|
26
|
+
"auto": "^10.37.6",
|
|
27
|
+
"eslint": "^8.25.0",
|
|
28
|
+
"eslint-config-prettier": "^8.5.0",
|
|
29
|
+
"eslint-plugin-prettier": "^4.2.1",
|
|
30
|
+
"husky": "^7.0.4",
|
|
31
|
+
"jest": "^29.7.0",
|
|
32
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
33
|
+
"lint-staged": "11",
|
|
34
|
+
"prettier": "^2.7.1",
|
|
35
|
+
"pull-lock": "^1.0.0",
|
|
36
|
+
"react": "^18.2.0",
|
|
37
|
+
"react-test-renderer": "^18.2.0",
|
|
38
|
+
"ts-jest": "^29.0.3",
|
|
39
|
+
"typescript": "^4.8.3"
|
|
40
|
+
},
|
|
41
|
+
"pull-lock": {
|
|
42
|
+
"yarn.lock": "yarn install"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"lodash.uniqby": "^4.7.0",
|
|
46
|
+
"yup": "^1.3.3"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import * as Yup from "yup"
|
|
2
|
+
import React, {
|
|
3
|
+
createContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react"
|
|
9
|
+
import uniqBy from "lodash.uniqby"
|
|
10
|
+
|
|
11
|
+
export type DismissibleKey = DismissibleContextProps["keys"][number]
|
|
12
|
+
export type DismissibleKeys = DismissibleKey[]
|
|
13
|
+
|
|
14
|
+
interface DismissedKey {
|
|
15
|
+
key: DismissibleKey
|
|
16
|
+
timestamp: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface DismissedKeyStatus {
|
|
20
|
+
status: boolean
|
|
21
|
+
timestamp: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DismissibleContextProps {
|
|
25
|
+
dismissed: DismissedKey[]
|
|
26
|
+
dismiss: (key: DismissibleKey | readonly DismissibleKey[]) => void
|
|
27
|
+
isDismissed: (key: DismissibleKey) => DismissedKeyStatus
|
|
28
|
+
keys: string[]
|
|
29
|
+
syncFromLoggedOutUser: () => void
|
|
30
|
+
/** An optional userID to track against */
|
|
31
|
+
userID?: string | null
|
|
32
|
+
__internal__: ReturnType<typeof useLocalStorageUtils>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const DismissibleContext = createContext<DismissibleContextProps>({
|
|
36
|
+
dismissed: [],
|
|
37
|
+
keys: [],
|
|
38
|
+
isDismissed: () => ({ status: false, timestamp: 0 }),
|
|
39
|
+
} as unknown as DismissibleContextProps)
|
|
40
|
+
|
|
41
|
+
export const DismissibleProvider: React.FC<{
|
|
42
|
+
children: React.ReactNode
|
|
43
|
+
keys: DismissibleKeys
|
|
44
|
+
userID?: DismissibleContextProps["userID"]
|
|
45
|
+
}> = ({ children, keys = [], userID }) => {
|
|
46
|
+
const id = userID ?? PROGRESSIVE_ONBOARDING_LOGGED_OUT_USER_ID
|
|
47
|
+
|
|
48
|
+
const [dismissed, setDismissed] = useState<DismissedKey[]>([])
|
|
49
|
+
|
|
50
|
+
const localStorageUtils = useLocalStorageUtils({ keys })
|
|
51
|
+
const { __dismiss__, get } = localStorageUtils
|
|
52
|
+
|
|
53
|
+
const dismiss = useCallback(
|
|
54
|
+
(key: DismissibleKey | DismissibleKey[]) => {
|
|
55
|
+
const keys = Array.isArray(key) ? key : [key]
|
|
56
|
+
const timestamp = Date.now()
|
|
57
|
+
|
|
58
|
+
__dismiss__(id, timestamp, keys)
|
|
59
|
+
|
|
60
|
+
setDismissed((prevDismissed) => {
|
|
61
|
+
return uniqBy(
|
|
62
|
+
[...prevDismissed, ...keys.map((k) => ({ key: k, timestamp }))],
|
|
63
|
+
(d) => d.key
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
[id]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
setDismissed(get(id))
|
|
72
|
+
}, [id])
|
|
73
|
+
|
|
74
|
+
const mounted = useDidMount()
|
|
75
|
+
|
|
76
|
+
const isDismissed = useCallback(
|
|
77
|
+
(key: DismissibleKey) => {
|
|
78
|
+
if (!mounted) {
|
|
79
|
+
return {
|
|
80
|
+
status: false,
|
|
81
|
+
timestamp: 0,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const dismissedKey = dismissed.find((d) => d.key === key)
|
|
86
|
+
|
|
87
|
+
return dismissedKey
|
|
88
|
+
? { status: true, timestamp: dismissedKey.timestamp }
|
|
89
|
+
: { status: false, timestamp: 0 }
|
|
90
|
+
},
|
|
91
|
+
[dismissed, mounted]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* If the user is logged out, and performs some action which causes them
|
|
96
|
+
* to login, we need to sync up the dismissed state from the logged out user
|
|
97
|
+
*/
|
|
98
|
+
const syncFromLoggedOutUser = useCallback(() => {
|
|
99
|
+
if (id === PROGRESSIVE_ONBOARDING_LOGGED_OUT_USER_ID) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const loggedOutDismissals = get(PROGRESSIVE_ONBOARDING_LOGGED_OUT_USER_ID)
|
|
104
|
+
const loggedInDismissals = get(id)
|
|
105
|
+
|
|
106
|
+
const dismissals = uniqBy(
|
|
107
|
+
[...loggedOutDismissals, ...loggedInDismissals],
|
|
108
|
+
(d) => d.key
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
setDismissed(dismissals)
|
|
112
|
+
|
|
113
|
+
localStorage.setItem(localStorageKey(id), JSON.stringify(dismissals))
|
|
114
|
+
localStorage.removeItem(
|
|
115
|
+
localStorageKey(PROGRESSIVE_ONBOARDING_LOGGED_OUT_USER_ID)
|
|
116
|
+
)
|
|
117
|
+
}, [id])
|
|
118
|
+
|
|
119
|
+
// Ensure that the dismissed state stays in sync incase the user
|
|
120
|
+
// has multiple tabs open.
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const current = get(id)
|
|
123
|
+
|
|
124
|
+
if (current.length === 0) return
|
|
125
|
+
|
|
126
|
+
const handleFocus = () => {
|
|
127
|
+
setDismissed(current)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
window.addEventListener("focus", handleFocus)
|
|
131
|
+
|
|
132
|
+
return () => {
|
|
133
|
+
window.removeEventListener("focus", handleFocus)
|
|
134
|
+
}
|
|
135
|
+
}, [id])
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<DismissibleContext.Provider
|
|
139
|
+
value={{
|
|
140
|
+
dismissed,
|
|
141
|
+
dismiss,
|
|
142
|
+
isDismissed,
|
|
143
|
+
keys,
|
|
144
|
+
syncFromLoggedOutUser,
|
|
145
|
+
__internal__: localStorageUtils,
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{children}
|
|
149
|
+
</DismissibleContext.Provider>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const useDismissibleContext = () => {
|
|
154
|
+
return useContext(DismissibleContext)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const PROGRESSIVE_ONBOARDING_LOGGED_OUT_USER_ID = "user" as const
|
|
158
|
+
|
|
159
|
+
export const localStorageKey = (id: string) => {
|
|
160
|
+
return `progressive-onboarding.dismissed.${id}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface UseLocalStorageUtilsProps {
|
|
164
|
+
keys: DismissibleContextProps["keys"]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const useLocalStorageUtils = ({ keys }: UseLocalStorageUtilsProps) => {
|
|
168
|
+
const schema = Yup.object().shape({
|
|
169
|
+
key: Yup.string().oneOf([...keys]),
|
|
170
|
+
timestamp: Yup.number(),
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const isValid = (value: DismissedKey): value is DismissedKey => {
|
|
174
|
+
return schema.isValidSync(value)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const parse = (value: string | null): DismissedKey[] => {
|
|
178
|
+
if (!value) return []
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const parsed = JSON.parse(value)
|
|
182
|
+
|
|
183
|
+
return parsed.filter((obj: DismissedKey) => {
|
|
184
|
+
return isValid(obj) && keys.includes(obj.key)
|
|
185
|
+
})
|
|
186
|
+
} catch (err) {
|
|
187
|
+
return []
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const __dismiss__ = (
|
|
192
|
+
id: string,
|
|
193
|
+
timestamp: number,
|
|
194
|
+
key: DismissibleKey | DismissibleKey[]
|
|
195
|
+
) => {
|
|
196
|
+
const keys = Array.isArray(key) ? key : [key]
|
|
197
|
+
|
|
198
|
+
keys.forEach((key) => {
|
|
199
|
+
const item = localStorage.getItem(localStorageKey(id))
|
|
200
|
+
const dismissed = parse(item)
|
|
201
|
+
|
|
202
|
+
localStorage.setItem(
|
|
203
|
+
localStorageKey(id),
|
|
204
|
+
JSON.stringify(
|
|
205
|
+
uniqBy([...dismissed, { key, timestamp }], ({ key }) => key)
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const get = (id: string) => {
|
|
212
|
+
const item = localStorage.getItem(localStorageKey(id))
|
|
213
|
+
return parse(item)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const reset = (id: string) => {
|
|
217
|
+
localStorage.removeItem(localStorageKey(id))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
__dismiss__,
|
|
222
|
+
get,
|
|
223
|
+
isValid,
|
|
224
|
+
parse,
|
|
225
|
+
reset,
|
|
226
|
+
schema,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function useDidMount(defaultMounted = false) {
|
|
231
|
+
const [isMounted, toggleMounted] = useState(defaultMounted)
|
|
232
|
+
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
toggleMounted(true)
|
|
235
|
+
}, [])
|
|
236
|
+
|
|
237
|
+
return isMounted
|
|
238
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { renderHook } from "@testing-library/react-hooks"
|
|
3
|
+
import {
|
|
4
|
+
PROGRESSIVE_ONBOARDING_LOGGED_OUT_USER_ID,
|
|
5
|
+
DismissibleProvider,
|
|
6
|
+
useLocalStorageUtils,
|
|
7
|
+
useDismissibleContext,
|
|
8
|
+
localStorageKey,
|
|
9
|
+
} from "../DismissibleContext"
|
|
10
|
+
|
|
11
|
+
describe("DismissibleContext", () => {
|
|
12
|
+
const keys = ["follow-artist", "follow-find", "follow-highlight"]
|
|
13
|
+
const id = "example-id"
|
|
14
|
+
|
|
15
|
+
const { get, reset, parse, __dismiss__ } = useLocalStorageUtils({
|
|
16
|
+
keys,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe("get", () => {
|
|
20
|
+
afterEach(() => reset(id))
|
|
21
|
+
|
|
22
|
+
it("returns an empty array if there is no value in local storage", () => {
|
|
23
|
+
expect(get(id)).toEqual([])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("returns empty array for the old format", () => {
|
|
27
|
+
localStorage.setItem(
|
|
28
|
+
localStorageKey(id),
|
|
29
|
+
JSON.stringify(["follow-artist"])
|
|
30
|
+
)
|
|
31
|
+
expect(get(id)).toEqual([])
|
|
32
|
+
|
|
33
|
+
localStorage.setItem(
|
|
34
|
+
localStorageKey(id),
|
|
35
|
+
JSON.stringify(["follow-artist", "follow-find"])
|
|
36
|
+
)
|
|
37
|
+
expect(get(id)).toEqual([])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("returns the all dismissed keys if there is a value in local storage", () => {
|
|
41
|
+
__dismiss__(id, 999, "follow-artist")
|
|
42
|
+
expect(get(id)).toEqual([{ key: "follow-artist", timestamp: 999 }])
|
|
43
|
+
__dismiss__(id, 444, "follow-find")
|
|
44
|
+
expect(get(id)).toEqual([
|
|
45
|
+
{ key: "follow-artist", timestamp: 999 },
|
|
46
|
+
{ key: "follow-find", timestamp: 444 },
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("does not return duplicate keys", () => {
|
|
51
|
+
__dismiss__(id, 555, "follow-artist")
|
|
52
|
+
expect(get(id)).toEqual([{ key: "follow-artist", timestamp: 555 }])
|
|
53
|
+
__dismiss__(id, 555, "follow-artist")
|
|
54
|
+
expect(get(id)).toEqual([{ key: "follow-artist", timestamp: 555 }])
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe("parse", () => {
|
|
59
|
+
it("returns an empty array if the value is null", () => {
|
|
60
|
+
expect(parse(null)).toEqual([])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("returns an empty array if the value is not an array", () => {
|
|
64
|
+
expect(parse("foo")).toEqual([])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("returns an empty array if the value is an array of non-strings", () => {
|
|
68
|
+
expect(parse(JSON.stringify([1, 2, 3]))).toEqual([])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("returns an empty array if the value is an array of strings that are not valid keys", () => {
|
|
72
|
+
expect(parse(JSON.stringify(["foo", "bar", "baz"]))).toEqual([])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("returns an array of valid keys if the value is an array of strings that are valid keys", () => {
|
|
76
|
+
expect(
|
|
77
|
+
parse(
|
|
78
|
+
JSON.stringify([
|
|
79
|
+
{ key: "follow-artist", timestamp: 555 },
|
|
80
|
+
{ key: "follow-find", timestamp: 555 },
|
|
81
|
+
{ key: "follow-highlight", timestamp: 555 },
|
|
82
|
+
])
|
|
83
|
+
)
|
|
84
|
+
).toEqual([
|
|
85
|
+
{ key: "follow-artist", timestamp: 555 },
|
|
86
|
+
{ key: "follow-find", timestamp: 555 },
|
|
87
|
+
{ key: "follow-highlight", timestamp: 555 },
|
|
88
|
+
])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("returns only the valid keys", () => {
|
|
92
|
+
expect(
|
|
93
|
+
parse(
|
|
94
|
+
JSON.stringify([
|
|
95
|
+
{ key: "follow-artist", timestamp: 555 },
|
|
96
|
+
{ key: "follow-find", timestamp: 555 },
|
|
97
|
+
{ key: "follow-highlight", timestamp: 555 },
|
|
98
|
+
"foo",
|
|
99
|
+
{ key: "no", timestamp: 555 },
|
|
100
|
+
{ key: "alert-create", timestamp: "wrong" },
|
|
101
|
+
"baz",
|
|
102
|
+
1,
|
|
103
|
+
2,
|
|
104
|
+
true,
|
|
105
|
+
false,
|
|
106
|
+
null,
|
|
107
|
+
undefined,
|
|
108
|
+
])
|
|
109
|
+
)
|
|
110
|
+
).toEqual([
|
|
111
|
+
{ key: "follow-artist", timestamp: 555 },
|
|
112
|
+
{ key: "follow-find", timestamp: 555 },
|
|
113
|
+
{ key: "follow-highlight", timestamp: 555 },
|
|
114
|
+
])
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe("__dismiss__", () => {
|
|
119
|
+
afterEach(() => reset(id))
|
|
120
|
+
|
|
121
|
+
it("adds the key to local storage", () => {
|
|
122
|
+
__dismiss__(id, 555, "follow-artist")
|
|
123
|
+
expect(get(id)).toEqual([{ key: "follow-artist", timestamp: 555 }])
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("adds multiple keys to local storage", () => {
|
|
127
|
+
__dismiss__(id, 555, ["follow-artist", "follow-find"])
|
|
128
|
+
expect(get(id)).toEqual([
|
|
129
|
+
{ key: "follow-artist", timestamp: 555 },
|
|
130
|
+
{ key: "follow-find", timestamp: 555 },
|
|
131
|
+
])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("does not add duplicate keys to local storage", () => {
|
|
135
|
+
__dismiss__(id, 555, "follow-artist")
|
|
136
|
+
expect(get(id)).toEqual([{ key: "follow-artist", timestamp: 555 }])
|
|
137
|
+
__dismiss__(id, 555, "follow-artist")
|
|
138
|
+
expect(get(id)).toEqual([{ key: "follow-artist", timestamp: 555 }])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('handles subsequent calls to "dismiss"', () => {
|
|
142
|
+
__dismiss__(id, 555, "follow-artist")
|
|
143
|
+
expect(get(id)).toEqual([{ key: "follow-artist", timestamp: 555 }])
|
|
144
|
+
__dismiss__(id, 555, "follow-find")
|
|
145
|
+
expect(get(id)).toEqual([
|
|
146
|
+
{ key: "follow-artist", timestamp: 555 },
|
|
147
|
+
{ key: "follow-find", timestamp: 555 },
|
|
148
|
+
])
|
|
149
|
+
__dismiss__(id, 555, "follow-highlight")
|
|
150
|
+
expect(get(id)).toEqual([
|
|
151
|
+
{ key: "follow-artist", timestamp: 555 },
|
|
152
|
+
{ key: "follow-find", timestamp: 555 },
|
|
153
|
+
{ key: "follow-highlight", timestamp: 555 },
|
|
154
|
+
])
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe("dismiss", () => {
|
|
159
|
+
afterEach(() => reset(id))
|
|
160
|
+
|
|
161
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
162
|
+
<DismissibleProvider keys={keys} userID={id}>
|
|
163
|
+
{children}
|
|
164
|
+
</DismissibleProvider>
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
it("dismisses keys", () => {
|
|
168
|
+
const { result } = renderHook(useDismissibleContext, {
|
|
169
|
+
wrapper,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
result.current.dismiss("follow-artist")
|
|
173
|
+
|
|
174
|
+
expect(result.current.isDismissed("follow-artist")).toEqual({
|
|
175
|
+
status: true,
|
|
176
|
+
timestamp: expect.any(Number),
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
expect(result.current.isDismissed("follow-find")).toEqual({
|
|
180
|
+
status: false,
|
|
181
|
+
timestamp: 0,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
expect(result.current.isDismissed("follow-highlight")).toEqual({
|
|
185
|
+
status: false,
|
|
186
|
+
timestamp: 0,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
expect(get(id)).toEqual([
|
|
190
|
+
{ key: "follow-artist", timestamp: expect.any(Number) },
|
|
191
|
+
])
|
|
192
|
+
|
|
193
|
+
result.current.dismiss(["follow-find", "follow-highlight"])
|
|
194
|
+
|
|
195
|
+
expect(result.current.isDismissed("follow-artist")).toEqual({
|
|
196
|
+
status: true,
|
|
197
|
+
timestamp: expect.any(Number),
|
|
198
|
+
})
|
|
199
|
+
expect(result.current.isDismissed("follow-find")).toEqual({
|
|
200
|
+
status: true,
|
|
201
|
+
timestamp: expect.any(Number),
|
|
202
|
+
})
|
|
203
|
+
expect(result.current.isDismissed("follow-highlight")).toEqual({
|
|
204
|
+
status: true,
|
|
205
|
+
timestamp: expect.any(Number),
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
expect(get(id)).toEqual([
|
|
209
|
+
{ key: "follow-artist", timestamp: expect.any(Number) },
|
|
210
|
+
{ key: "follow-find", timestamp: expect.any(Number) },
|
|
211
|
+
{ key: "follow-highlight", timestamp: expect.any(Number) },
|
|
212
|
+
])
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe("syncFromLoggedOutUser", () => {
|
|
217
|
+
it("does nothing if the user is logged out", () => {
|
|
218
|
+
const { result } = renderHook(useDismissibleContext, {
|
|
219
|
+
wrapper: ({ children }: { children: React.ReactNode }) => (
|
|
220
|
+
<DismissibleProvider keys={keys} userID={id}>
|
|
221
|
+
{children}
|
|
222
|
+
</DismissibleProvider>
|
|
223
|
+
),
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
result.current.syncFromLoggedOutUser()
|
|
227
|
+
|
|
228
|
+
expect(get(id)).toEqual([])
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe("logged in", () => {
|
|
232
|
+
it("syncs the dismissed state from the logged out user", () => {
|
|
233
|
+
const loggedOutUserId = PROGRESSIVE_ONBOARDING_LOGGED_OUT_USER_ID
|
|
234
|
+
|
|
235
|
+
const loggedOutDismissals = [
|
|
236
|
+
{ key: "follow-artist", timestamp: 555 },
|
|
237
|
+
{ key: "follow-find", timestamp: 555 },
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
localStorage.setItem(
|
|
241
|
+
localStorageKey(loggedOutUserId),
|
|
242
|
+
JSON.stringify(loggedOutDismissals)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
expect(get(id)).toEqual([])
|
|
246
|
+
|
|
247
|
+
const { result } = renderHook(useDismissibleContext, {
|
|
248
|
+
wrapper: ({ children }: { children: React.ReactNode }) => (
|
|
249
|
+
<DismissibleProvider keys={keys} userID={id}>
|
|
250
|
+
{children}
|
|
251
|
+
</DismissibleProvider>
|
|
252
|
+
),
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
result.current.syncFromLoggedOutUser()
|
|
256
|
+
|
|
257
|
+
expect(get(id)).toEqual(loggedOutDismissals)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
})
|
package/src/index.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowJs": false,
|
|
4
|
+
"allowSyntheticDefaultImports": true,
|
|
5
|
+
"baseUrl": "src",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"jsxImportSource": "react",
|
|
11
|
+
"lib": ["ES6", "DOM", "ES2016", "ES2017"],
|
|
12
|
+
"module": "CommonJS",
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"noImplicitAny": true,
|
|
15
|
+
"noImplicitReturns": true,
|
|
16
|
+
"noImplicitThis": true,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"outDir": "dist",
|
|
20
|
+
"rootDir": "src",
|
|
21
|
+
"sourceMap": true,
|
|
22
|
+
"strictNullChecks": true,
|
|
23
|
+
"target": "ES6"
|
|
24
|
+
},
|
|
25
|
+
"include": ["src/**/*.ts"],
|
|
26
|
+
"exclude": ["node_modules", "dist"]
|
|
27
|
+
}
|