@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 ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "@artsy"
3
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ . "$(dirname "$0")/_/husky.sh"
3
+
4
+ yarn pull-lock
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ . "$(dirname "$0")/_/husky.sh"
3
+
4
+ yarn lint-staged
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ . "$(dirname "$0")/_/husky.sh"
3
+
4
+ yarn type-check
@@ -0,0 +1,6 @@
1
+ {
2
+ "*.ts?(x)": [
3
+ "eslint --fix",
4
+ "prettier --write"
5
+ ]
6
+ }
package/.prettierrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "trailingComma": "es5",
3
+ "tabWidth": 2,
4
+ "semi": false,
5
+ "singleQuote": false
6
+ }
@@ -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
@@ -0,0 +1,7 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ moduleDirectories: ["node_modules", "src"],
4
+ preset: "ts-jest",
5
+ rootDir: "src",
6
+ testEnvironment: "jest-environment-jsdom",
7
+ }
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
@@ -0,0 +1,4 @@
1
+ export {
2
+ DismissibleProvider,
3
+ useDismissibleContext,
4
+ } from "./DismissibleContext"
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": [
4
+ "src/**/__tests__/*"
5
+ ]
6
+ }
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
+ }