@pushwoosh/dumb-components 4.3.4 → 4.4.1

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/index.d.ts CHANGED
@@ -19,7 +19,6 @@ export { ProgressBar } from './ProgressBar';
19
19
  export { Popover } from './Popover';
20
20
  export { Radio } from './Radio';
21
21
  export { RadioCard } from './RadioCard';
22
- export { ReCaptcha, useRecaptcha, asyncScriptLoad } from './ReCaptcha';
23
22
  export { Spinner } from './Spinner';
24
23
  export { SortButton, type SortButtonParams } from './SortButton';
25
24
  export { SplitButton, type SplitButtonProps } from './SplitButton';
package/index.js CHANGED
@@ -19,7 +19,6 @@ export { ProgressBar } from './ProgressBar';
19
19
  export { Popover } from './Popover';
20
20
  export { Radio } from './Radio';
21
21
  export { RadioCard } from './RadioCard';
22
- export { ReCaptcha, useRecaptcha, asyncScriptLoad } from './ReCaptcha';
23
22
  export { Spinner } from './Spinner';
24
23
  export { SortButton } from './SortButton';
25
24
  export { SplitButton } from './SplitButton';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "4.3.4",
3
+ "version": "4.4.1",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -12,9 +12,12 @@ export function useAsyncItems({
12
12
  debounceMs = DEFAULT_DEBOUNCE_MS,
13
13
  minChars = 0
14
14
  }) {
15
- const [items, setItems] = useState([]);
16
- const [loading, setLoading] = useState(false);
17
- const [loadingMore, setLoadingMore] = useState(false);
15
+ const [state, setState] = useState({
16
+ session: 0,
17
+ items: [],
18
+ loading: false,
19
+ loadingMore: false
20
+ });
18
21
  const debouncedQuery = useDebouncedValue(query, debounceMs);
19
22
  const loadItemsRef = useRef(loadItems);
20
23
  loadItemsRef.current = loadItems;
@@ -23,60 +26,85 @@ export function useAsyncItems({
23
26
  const queryRef = useRef('');
24
27
  const loadingRef = useRef(false);
25
28
  const hasMoreRef = useRef(false);
29
+ const sessionDeps = [debouncedQuery, isOpen, minChars];
30
+ const [prevSessionDeps, setPrevSessionDeps] = useState(null);
31
+ if (prevSessionDeps === null || sessionDeps.some((dep, index) => dep !== prevSessionDeps[index])) {
32
+ setPrevSessionDeps(sessionDeps);
33
+ const belowMin = debouncedQuery.trim().length < minChars;
34
+ setState(prev => ({
35
+ session: prev.session + 1,
36
+ items: !loadItems || belowMin ? [] : prev.items,
37
+ loading: Boolean(loadItems) && isOpen && !belowMin,
38
+ loadingMore: false
39
+ }));
40
+ }
41
+ const {
42
+ session
43
+ } = state;
26
44
  useEffect(() => {
27
45
  const loader = loadItemsRef.current;
28
46
  const belowMin = debouncedQuery.trim().length < minChars;
29
- sessionRef.current += 1;
47
+ sessionRef.current = session;
30
48
  pageRef.current = 0;
31
49
  loadingRef.current = false;
32
50
  hasMoreRef.current = false;
33
- setLoadingMore(false);
34
51
  if (!loader || !isOpen || belowMin) {
35
- setLoading(false);
36
- if (!loader || belowMin) setItems([]);
37
52
  return undefined;
38
53
  }
39
- const session = sessionRef.current;
40
54
  queryRef.current = debouncedQuery;
41
55
  loadingRef.current = true;
42
- setLoading(true);
43
56
  Promise.resolve(loader(debouncedQuery, 0)).then(result => {
44
57
  if (session !== sessionRef.current) return;
45
- setItems(result.items);
46
58
  hasMoreRef.current = result.hasMore ?? false;
47
59
  loadingRef.current = false;
48
- setLoading(false);
60
+ setState(prev => prev.session === session ? {
61
+ ...prev,
62
+ items: result.items,
63
+ loading: false
64
+ } : prev);
49
65
  }).catch(() => {
50
66
  if (session !== sessionRef.current) return;
51
67
  loadingRef.current = false;
52
- setLoading(false);
68
+ setState(prev => prev.session === session ? {
69
+ ...prev,
70
+ loading: false
71
+ } : prev);
53
72
  });
54
73
  return undefined;
55
- }, [debouncedQuery, isOpen, minChars]);
74
+ }, [session, debouncedQuery, isOpen, minChars]);
56
75
  const loadMore = useCallback(() => {
57
76
  const loader = loadItemsRef.current;
58
77
  if (!loader || loadingRef.current || !hasMoreRef.current) return;
59
- const session = sessionRef.current;
78
+ const loadSession = sessionRef.current;
60
79
  const nextPage = pageRef.current + 1;
61
80
  loadingRef.current = true;
62
- setLoadingMore(true);
81
+ setState(prev => prev.session === loadSession ? {
82
+ ...prev,
83
+ loadingMore: true
84
+ } : prev);
63
85
  Promise.resolve(loader(queryRef.current, nextPage)).then(result => {
64
- if (session !== sessionRef.current) return;
86
+ if (loadSession !== sessionRef.current) return;
65
87
  pageRef.current = nextPage;
66
- setItems(prev => [...prev, ...result.items]);
67
88
  hasMoreRef.current = result.hasMore ?? false;
68
89
  loadingRef.current = false;
69
- setLoadingMore(false);
90
+ setState(prev => prev.session === loadSession ? {
91
+ ...prev,
92
+ items: [...prev.items, ...result.items],
93
+ loadingMore: false
94
+ } : prev);
70
95
  }).catch(() => {
71
- if (session !== sessionRef.current) return;
96
+ if (loadSession !== sessionRef.current) return;
72
97
  loadingRef.current = false;
73
- setLoadingMore(false);
98
+ setState(prev => prev.session === loadSession ? {
99
+ ...prev,
100
+ loadingMore: false
101
+ } : prev);
74
102
  });
75
103
  }, []);
76
104
  return {
77
- items,
78
- loading,
79
- loadingMore,
105
+ items: state.items,
106
+ loading: state.loading,
107
+ loadingMore: state.loadingMore,
80
108
  loadMore
81
109
  };
82
110
  }
@@ -1,10 +0,0 @@
1
- import { type ReactElement } from 'react';
2
- /**
3
- * Renders the (invisible) reCAPTCHA host element. Pair with {@link useRecaptcha} to
4
- * load the script and obtain tokens.
5
- *
6
- * @example
7
- * const { getRecaptchaToken } = useRecaptcha();
8
- * return <ReCaptcha />;
9
- */
10
- export declare function ReCaptcha(): ReactElement;
@@ -1,16 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { RECAPTCHA_CONTAINER_ID } from './constants';
3
- import { RecaptchaElement } from './styles';
4
- /**
5
- * Renders the (invisible) reCAPTCHA host element. Pair with {@link useRecaptcha} to
6
- * load the script and obtain tokens.
7
- *
8
- * @example
9
- * const { getRecaptchaToken } = useRecaptcha();
10
- * return <ReCaptcha />;
11
- */
12
- export function ReCaptcha() {
13
- return _jsx(RecaptchaElement, {
14
- id: RECAPTCHA_CONTAINER_ID
15
- });
16
- }
@@ -1,3 +0,0 @@
1
- export declare const RECAPTCHA_KEY = "6LdS7DYUAAAAABVgt0djIBoyyMQ0oDAUl7jt1Mxh";
2
- export declare const RECAPTCHA_SCRIPT_URL = "https://www.google.com/recaptcha/api.js?render=explicit";
3
- export declare const RECAPTCHA_CONTAINER_ID = "g-recaptcha";
@@ -1,4 +0,0 @@
1
- export const RECAPTCHA_KEY = '6LdS7DYUAAAAABVgt0djIBoyyMQ0oDAUl7jt1Mxh';
2
- // onload script: https://www.google.com/recaptcha/api.js?onload=recaptchaOnLoad&render=explicit'
3
- export const RECAPTCHA_SCRIPT_URL = 'https://www.google.com/recaptcha/api.js?render=explicit';
4
- export const RECAPTCHA_CONTAINER_ID = 'g-recaptcha';
@@ -1,5 +0,0 @@
1
- /**
2
- * Injects a `<script>` with the given `src` once and resolves when it loads
3
- * (resolves immediately if already present); rejects on load error.
4
- */
5
- export declare function asyncScriptLoad(scriptUrl: string): Promise<Event | void>;
@@ -1,19 +0,0 @@
1
- /**
2
- * Injects a `<script>` with the given `src` once and resolves when it loads
3
- * (resolves immediately if already present); rejects on load error.
4
- */
5
- export async function asyncScriptLoad(scriptUrl) {
6
- return new Promise((resolve, reject) => {
7
- const isAlreadyLoaded = document.querySelector(`script[src="${scriptUrl}"]`);
8
- if (isAlreadyLoaded) {
9
- resolve();
10
- return;
11
- }
12
- const script = document.createElement('script');
13
- script.src = scriptUrl;
14
- script.async = true;
15
- document.body.appendChild(script);
16
- script.onload = resolve;
17
- script.onerror = reject;
18
- });
19
- }
@@ -1,6 +0,0 @@
1
- import type { RecaptchaExposedApi } from './types';
2
- /**
3
- * Loads the reCAPTCHA script, renders an invisible widget into the {@link ReCaptcha}
4
- * element, and returns `{ isRecaptchaReady, recaptchaErrorCode, getRecaptchaToken }`.
5
- */
6
- export declare function useRecaptcha(): RecaptchaExposedApi;
@@ -1,63 +0,0 @@
1
- import { useState, useEffect, useCallback, useRef } from 'react';
2
- import { RECAPTCHA_KEY, RECAPTCHA_SCRIPT_URL, RECAPTCHA_CONTAINER_ID } from './constants';
3
- import { asyncScriptLoad } from './helpers';
4
- /**
5
- * Loads the reCAPTCHA script, renders an invisible widget into the {@link ReCaptcha}
6
- * element, and returns `{ isRecaptchaReady, recaptchaErrorCode, getRecaptchaToken }`.
7
- */
8
- export function useRecaptcha() {
9
- const promiseRef = useRef({});
10
- const [isRecaptchaLoaded, setRecaptchaLoaded] = useState(!!window?.grecaptcha?.render);
11
- const [widgetID, setWidgetID] = useState(null);
12
- const [isRecaptchaReady, setRecaptchaReady] = useState(!!(isRecaptchaLoaded && widgetID));
13
- const [recaptchaErrorCode, setRecaptchaErrorCode] = useState(''); // RIEXXX - ReCaptcha Internal Error
14
- // Load external script effect, fires once
15
- useEffect(() => {
16
- asyncScriptLoad(RECAPTCHA_SCRIPT_URL).catch(() => setRecaptchaErrorCode('RIE001'));
17
- }, []);
18
- // Set interval while library download and initialize
19
- useEffect(() => {
20
- const newInterval = window.setInterval(() => {
21
- if (window?.grecaptcha?.render) {
22
- setRecaptchaLoaded(true);
23
- clearInterval(newInterval);
24
- }
25
- }, 500);
26
- return () => {
27
- clearInterval(newInterval);
28
- };
29
- }, []);
30
- // Render recaptcha widget when script loaded, stop interval
31
- useEffect(() => {
32
- if (isRecaptchaLoaded && widgetID === null) {
33
- try {
34
- const widget = window.grecaptcha.render(RECAPTCHA_CONTAINER_ID, {
35
- sitekey: RECAPTCHA_KEY,
36
- size: 'invisible',
37
- callback: token => promiseRef.current?.resolve(token)
38
- // 'expired-callback': () => {},
39
- // 'error-callback': () => {},
40
- });
41
- setWidgetID(widget);
42
- setRecaptchaReady(true);
43
- } catch {
44
- setRecaptchaErrorCode('RIE002');
45
- }
46
- }
47
- }, [isRecaptchaLoaded, widgetID, promiseRef]);
48
- // async function to get recaptcha token, resolves by resolveAction callback
49
- const getRecaptchaToken = useCallback(async () => new Promise((resolve, reject) => {
50
- try {
51
- window.grecaptcha.reset(widgetID);
52
- window.grecaptcha.execute(widgetID);
53
- promiseRef.current.resolve = resolve;
54
- } catch (error) {
55
- reject(error);
56
- }
57
- }), [widgetID, promiseRef]);
58
- return {
59
- isRecaptchaReady,
60
- recaptchaErrorCode,
61
- getRecaptchaToken
62
- };
63
- }
@@ -1,3 +0,0 @@
1
- export { ReCaptcha } from './ReCaptcha';
2
- export { useRecaptcha } from './hooks';
3
- export { asyncScriptLoad } from './helpers';
@@ -1,3 +0,0 @@
1
- export { ReCaptcha } from './ReCaptcha';
2
- export { useRecaptcha } from './hooks';
3
- export { asyncScriptLoad } from './helpers';
@@ -1,3 +0,0 @@
1
- export declare const RecaptchaElement: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
2
- style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
3
- }> & string;
@@ -1,5 +0,0 @@
1
- import styled from 'styled-components';
2
- export const RecaptchaElement = styled.div.withConfig({
3
- displayName: "RecaptchaElement",
4
- componentId: "sc-100g1t3-0"
5
- })(["display:none;"]);
@@ -1,13 +0,0 @@
1
- /** Value returned by {@link useRecaptcha}. */
2
- export type RecaptchaExposedApi = {
3
- /** Whether the widget is rendered and ready to execute. */
4
- readonly isRecaptchaReady: boolean;
5
- /** Internal error code (`RIExxx`), or empty string when there's no error. */
6
- readonly recaptchaErrorCode: string;
7
- /** Runs an invisible challenge and resolves with the verification token. */
8
- readonly getRecaptchaToken: () => Promise<string>;
9
- };
10
- /** Holds the pending token resolver between `execute` and the reCAPTCHA callback. */
11
- export type PromiseRef = {
12
- resolve?: (token: string) => void;
13
- };
@@ -1 +0,0 @@
1
- export {};