mirror of
https://github.com/XFox111/TabsAsideExtension.git
synced 2026-04-22 07:58:01 +03:00
feat: Minor 3.1.0 (#150)
* Some features are now optional (#148) * fix(dev): yarn.lock tree fix * feat: bookmarks moved to optional permissions * fix: analytics not working in firefox * feat!: ability to turn off analytics (uses permissions on firefox) * feat: analytics tracker for bookmark export * feat: add privacy policy link in about section * docs: privacy policy update * feat: ability to chain multiple dialogs * fix(loc): analytics option translation * feat: settings review dialog * fix: background script fails to load because of frontend code * chore: use analytics permission as storage value * fix: inverted analytics value * feat!: option to disable thumbnail capture * fix(ci): sed typo * fix: minor fixes * fix(firefox): web-ext lint error fix * chore(ci): switch web-ext action * chore(lint): fix eslint warnings * chore(deps): monthly dependency bump (September 2025) (#149) * chore: 3.1.0 version bump * chore: minor cleanup * fix: allow analytics checkbox stays inactive after denying permission on firefox * fix(deps): yarn.lock rebuild * fix: type assertion for userId * fix: settings review dialog not showing if welcome dialog is not required * fix: analytics and thumbnail capture toggles react incorrectly if permission is denied
This commit is contained in:
@@ -1,3 +1,55 @@
|
||||
export { default as userPropertiesStorage } from "./utils/userPropertiesStorage";
|
||||
export { default as trackError } from "./utils/trackError";
|
||||
export { default as track } from "./utils/track";
|
||||
import { analytics } from "./utils/analytics";
|
||||
import analyticsPermission from "./utils/analyticsPermission";
|
||||
import { getUserProperties, userId } from "./utils/getUserProperties";
|
||||
|
||||
export { analyticsPermission };
|
||||
|
||||
export async function track(eventName: string, eventProperties?: Record<string, string>): Promise<void>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await analyticsPermission.getValue())
|
||||
return;
|
||||
|
||||
analytics.track(eventName, eventProperties);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
console.error("Failed to send analytics event", ex);
|
||||
}
|
||||
}
|
||||
|
||||
export async function trackError(name: string, error: Error): Promise<void>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await analyticsPermission.getValue())
|
||||
return;
|
||||
|
||||
analytics.track(name, {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack ?? "no_stack"
|
||||
});
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
console.error("Failed to send error report", ex);
|
||||
}
|
||||
}
|
||||
|
||||
export async function trackPage(pageName: string): Promise<void>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await analyticsPermission.getValue())
|
||||
return;
|
||||
|
||||
analytics.identify(await userId.getValue() as string, await getUserProperties());
|
||||
analytics.page(pageName);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
console.error("Failed to send page view", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createAnalytics } from "@wxt-dev/analytics";
|
||||
import { googleAnalytics4 } from "@wxt-dev/analytics/providers/google-analytics-4";
|
||||
|
||||
export const analytics = createAnalytics({
|
||||
providers:
|
||||
[
|
||||
googleAnalytics4({
|
||||
apiSecret: import.meta.env.WXT_GA4_API_SECRET,
|
||||
measurementId: import.meta.env.WXT_GA4_MEASUREMENT_ID
|
||||
})
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Unwatch, WatchCallback, WxtStorageItem } from "wxt/storage";
|
||||
import { analytics } from "./analytics";
|
||||
import { Permissions } from "wxt/browser";
|
||||
|
||||
const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> =
|
||||
{
|
||||
getValue: async (): Promise<boolean> =>
|
||||
{
|
||||
const isGranted: boolean = import.meta.env.FIREFOX
|
||||
? await browser.permissions.contains({
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
data_collection: ["technicalAndInteraction"]
|
||||
})
|
||||
: await allowAnalytics.getValue();
|
||||
|
||||
analytics.setEnabled(isGranted);
|
||||
|
||||
return isGranted;
|
||||
},
|
||||
|
||||
setValue: async (value: boolean) =>
|
||||
{
|
||||
if (!import.meta.env.FIREFOX)
|
||||
{
|
||||
await allowAnalytics.setValue(value);
|
||||
return;
|
||||
}
|
||||
|
||||
let result: boolean = false;
|
||||
|
||||
if (value)
|
||||
result = await browser.permissions.request({
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
data_collection: ["technicalAndInteraction"]
|
||||
});
|
||||
else
|
||||
result = await browser.permissions.remove({
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
data_collection: ["technicalAndInteraction"]
|
||||
});
|
||||
|
||||
if (!result)
|
||||
throw new Error("Permission request was denied");
|
||||
},
|
||||
|
||||
watch: (cb: WatchCallback<boolean>): Unwatch =>
|
||||
{
|
||||
if (!import.meta.env.FIREFOX)
|
||||
return allowAnalytics.watch(cb);
|
||||
|
||||
const listener = async (permissions: Permissions.Permissions): Promise<void> =>
|
||||
{
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
if (permissions.data_collection?.includes("technicalAndInteraction"))
|
||||
{
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
const isGranted: boolean = await browser.permissions.contains({ data_collection: ["technicalAndInteraction"] });
|
||||
cb(isGranted, !isGranted);
|
||||
}
|
||||
};
|
||||
|
||||
browser.permissions.onAdded.addListener(listener);
|
||||
browser.permissions.onRemoved.addListener(listener);
|
||||
|
||||
return (): void =>
|
||||
{
|
||||
browser.permissions.onAdded.removeListener(listener);
|
||||
browser.permissions.onRemoved.removeListener(listener);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default analyticsPermission;
|
||||
|
||||
const allowAnalytics = storage.defineItem<boolean>("local:analytics", {
|
||||
fallback: true
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { cloudDisabled, collectionCount } from "@/features/collectionStorage";
|
||||
import { settings } from "@/utils/settings";
|
||||
|
||||
export async function getUserProperties(): Promise<UserProperties>
|
||||
{
|
||||
const properties: UserProperties =
|
||||
{
|
||||
cloud_used: await cloudDisabled.getValue() ? "-1" : (await browser.storage.sync.getBytesInUse() / 102400).toString(),
|
||||
collection_count: (await collectionCount.getValue()).toString()
|
||||
};
|
||||
|
||||
for (const key of Object.keys(settings))
|
||||
{
|
||||
const value = await settings[key as keyof typeof settings].getValue();
|
||||
properties[`option_${key}`] = value.valueOf().toString();
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
export const userId = storage.defineItem("local:userId", {
|
||||
init: () => crypto.randomUUID()
|
||||
});
|
||||
|
||||
export type UserProperties =
|
||||
{
|
||||
collection_count: string;
|
||||
cloud_used: string;
|
||||
[key: `option_${string}`]: string;
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
export default function track(eventName: string, eventProperties?: Record<string, string>): void
|
||||
{
|
||||
try
|
||||
{
|
||||
analytics.track(eventName, eventProperties);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
console.error("Failed to send analytics event", ex);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
export default function trackError(name: string, error: Error): void
|
||||
{
|
||||
try
|
||||
{
|
||||
analytics.track(name, {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack ?? "no_stack"
|
||||
});
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
console.error("Failed to send error report", ex);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { cloudDisabled, collectionCount } from "@/features/collectionStorage";
|
||||
import { settings } from "@/utils/settings";
|
||||
import { WxtStorageItem } from "wxt/storage";
|
||||
|
||||
// @ts-expect-error we don't need to implement a full storage item
|
||||
const userPropertiesStorage: WxtStorageItem<Record<string, string>, any> =
|
||||
{
|
||||
getValue: async (): Promise<UserProperties> =>
|
||||
{
|
||||
console.log("userPropertiesStorage.getValue");
|
||||
const properties: UserProperties =
|
||||
{
|
||||
cloud_used: await cloudDisabled.getValue() ? "-1" : (await browser.storage.sync.getBytesInUse() / 102400).toString(),
|
||||
collection_count: (await collectionCount.getValue()).toString()
|
||||
};
|
||||
|
||||
for (const key of Object.keys(settings))
|
||||
{
|
||||
const value = await settings[key as keyof typeof settings].getValue();
|
||||
properties[`option_${key}`] = value.valueOf().toString();
|
||||
}
|
||||
|
||||
return properties;
|
||||
},
|
||||
setValue: async () => { }
|
||||
};
|
||||
|
||||
export default userPropertiesStorage;
|
||||
|
||||
export type UserProperties =
|
||||
{
|
||||
collection_count: string;
|
||||
cloud_used: string;
|
||||
[key: `option_${string}`]: string;
|
||||
};
|
||||
@@ -5,6 +5,9 @@ export { default as getCollections } from "./utils/getCollections";
|
||||
export { default as resoveConflict } from "./utils/resolveConflict";
|
||||
export { default as saveCollections } from "./utils/saveCollections";
|
||||
export { default as setCloudStorage } from "./utils/setCloudStorage";
|
||||
export { default as clearGraphicsStorage } from "./utils/clearGraphics";
|
||||
|
||||
export { default as thumbnailCaptureEnabled } from "./utils/thumbnailCaptureEnabled";
|
||||
|
||||
export const collectionCount = collectionStorage.count;
|
||||
export const graphics = collectionStorage.graphics;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
|
||||
export default async function clearGraphicsStorage(): Promise<void>
|
||||
{
|
||||
await collectionStorage.graphics.removeValue();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Permissions } from "wxt/browser";
|
||||
import { Unwatch, WatchCallback, WxtStorageItem } from "wxt/storage";
|
||||
|
||||
const thumbnailCaptureEnabled: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> =
|
||||
{
|
||||
getValue: async (): Promise<boolean> =>
|
||||
await browser.permissions.contains({ permissions: ["scripting"], origins: ["<all_urls>"] }),
|
||||
|
||||
watch: (cb: WatchCallback<boolean>): Unwatch =>
|
||||
{
|
||||
const listener = async (permissions: Permissions.Permissions): Promise<void> =>
|
||||
{
|
||||
if (permissions.permissions?.includes("scripting") || permissions.origins?.includes("<all_urls>"))
|
||||
{
|
||||
const isGranted: boolean = await browser.permissions.contains({ permissions: ["scripting"], origins: ["<all_urls>"] });
|
||||
console.log("thumbnailCaptureEnabled changed", isGranted);
|
||||
cb(isGranted, !isGranted);
|
||||
}
|
||||
};
|
||||
|
||||
browser.permissions.onAdded.addListener(listener);
|
||||
browser.permissions.onRemoved.addListener(listener);
|
||||
|
||||
return (): void =>
|
||||
{
|
||||
browser.permissions.onAdded.removeListener(listener);
|
||||
browser.permissions.onRemoved.removeListener(listener);
|
||||
};
|
||||
},
|
||||
|
||||
setValue: async (value: boolean): Promise<void> =>
|
||||
{
|
||||
let result: boolean = false;
|
||||
|
||||
if (value)
|
||||
result = await browser.permissions.request({ permissions: ["scripting"], origins: ["<all_urls>"] });
|
||||
else
|
||||
{
|
||||
result = await browser.permissions.remove({ origins: ["<all_urls>"] });
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
await browser.permissions.request({ origins: ["http://localhost/*"] });
|
||||
}
|
||||
|
||||
if (!result)
|
||||
throw new Error("Permission request was denied");
|
||||
}
|
||||
};
|
||||
|
||||
export default thumbnailCaptureEnabled;
|
||||
@@ -0,0 +1,132 @@
|
||||
import { githubLinks } from "@/data/links";
|
||||
import { analyticsPermission } from "@/features/analytics";
|
||||
import extLink from "@/utils/extLink";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { settingsForReview } from "../utils/showSettingsReviewDialog";
|
||||
import { reviewSettings } from "../utils/setSettingsReviewNeeded";
|
||||
import { Unwatch } from "wxt/storage";
|
||||
import { thumbnailCaptureEnabled } from "@/features/collectionStorage";
|
||||
|
||||
export default function SettingsReviewDialog(): React.ReactElement
|
||||
{
|
||||
const [allowAnalytics, setAllowAnalytics] = useState<boolean | null>(null);
|
||||
const [captureThumbnails, setCaptureThumbnails] = useState<boolean | null>(null);
|
||||
const [needsReview, setNeedsReview] = useState<string[]>([]);
|
||||
const cls = useStyles();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
analyticsPermission.getValue().then(setAllowAnalytics);
|
||||
thumbnailCaptureEnabled.getValue().then(setCaptureThumbnails);
|
||||
settingsForReview.getValue().then(setNeedsReview);
|
||||
|
||||
const unwatchAnalytics: Unwatch = analyticsPermission.watch(setAllowAnalytics);
|
||||
const unwatchThumbnails: Unwatch = thumbnailCaptureEnabled.watch(setCaptureThumbnails);
|
||||
|
||||
return () =>
|
||||
{
|
||||
unwatchAnalytics();
|
||||
unwatchThumbnails();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateAnalytics = (enabled: boolean): void =>
|
||||
{
|
||||
setAllowAnalytics(null);
|
||||
analyticsPermission.setValue(enabled)
|
||||
.catch(() => setAllowAnalytics(!enabled));
|
||||
};
|
||||
|
||||
const updateThumbnails = (enabled: boolean): void =>
|
||||
{
|
||||
setCaptureThumbnails(null);
|
||||
thumbnailCaptureEnabled.setValue(enabled)
|
||||
.catch(() => setCaptureThumbnails(!enabled));
|
||||
};
|
||||
|
||||
return (
|
||||
<fui.DialogSurface>
|
||||
<fui.DialogBody>
|
||||
<fui.DialogTitle>{ i18n.t("features.settingsReview.title") }</fui.DialogTitle>
|
||||
<fui.DialogContent className={ cls.content }>
|
||||
{ needsReview.includes(reviewSettings.THUMBNAILS) &&
|
||||
<div className={ cls.section }>
|
||||
<fui.Switch
|
||||
label={ i18n.t("options_page.storage.thumbnail_capture") }
|
||||
checked={ captureThumbnails ?? true }
|
||||
disabled={ captureThumbnails === null }
|
||||
onChange={ (_, e) => updateThumbnails(e.checked as boolean) } />
|
||||
|
||||
<fui.MessageBar layout="multiline">
|
||||
<fui.MessageBarBody className={ cls.msgBarBody }>
|
||||
<fui.MessageBarTitle>
|
||||
{ i18n.t("options_page.storage.thumbnail_capture_notice1") }
|
||||
</fui.MessageBarTitle>
|
||||
<fui.Text as="p">
|
||||
{ i18n.t("options_page.storage.thumbnail_capture_notice2") }
|
||||
</fui.Text>
|
||||
</fui.MessageBarBody>
|
||||
</fui.MessageBar>
|
||||
</div>
|
||||
}
|
||||
{ needsReview.includes(reviewSettings.ANALYTICS) &&
|
||||
<div className={ cls.section }>
|
||||
<fui.Switch
|
||||
label={ i18n.t("options_page.general.options.allow_analytics") }
|
||||
checked={ allowAnalytics ?? true }
|
||||
disabled={ allowAnalytics === null }
|
||||
onChange={ (_, e) => updateAnalytics(e.checked as boolean) } />
|
||||
|
||||
<fui.MessageBar layout="multiline">
|
||||
<fui.MessageBarBody className={ cls.msgBarBody }>
|
||||
<fui.MessageBarTitle>
|
||||
{ i18n.t("features.settingsReview.analytics.title") }
|
||||
</fui.MessageBarTitle>
|
||||
<fui.Text as="p">
|
||||
{ i18n.t("features.settingsReview.analytics.p1") }
|
||||
</fui.Text>
|
||||
<fui.Text as="p" weight="semibold">
|
||||
{ i18n.t("features.settingsReview.analytics.p2") }
|
||||
</fui.Text>
|
||||
<fui.Text as="p">
|
||||
{ i18n.t("features.settingsReview.analytics.p3_text") } <fui.Link { ...extLink(githubLinks.privacy) }>{ i18n.t("features.settingsReview.analytics.p3_link") }</fui.Link>.
|
||||
</fui.Text>
|
||||
</fui.MessageBarBody>
|
||||
</fui.MessageBar>
|
||||
</div>
|
||||
}
|
||||
</fui.DialogContent>
|
||||
<fui.DialogActions>
|
||||
<fui.Button onClick={ () => browser.runtime.openOptionsPage() }>
|
||||
{ i18n.t("features.settingsReview.action") }
|
||||
</fui.Button>
|
||||
<fui.DialogTrigger>
|
||||
<fui.Button appearance="primary">{ i18n.t("common.actions.save") }</fui.Button>
|
||||
</fui.DialogTrigger>
|
||||
</fui.DialogActions>
|
||||
</fui.DialogBody>
|
||||
</fui.DialogSurface>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = fui.makeStyles({
|
||||
content:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: fui.tokens.spacingVerticalL
|
||||
},
|
||||
section:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: fui.tokens.spacingVerticalXS
|
||||
},
|
||||
msgBarBody:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: fui.tokens.spacingVerticalXS,
|
||||
marginBottom: fui.tokens.spacingVerticalXS
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DialogContextType } from "@/contexts/DialogProvider";
|
||||
import SettingsReviewDialog from "../components/SettingsReviewDialog";
|
||||
import { settingsForReview } from "../utils/showSettingsReviewDialog";
|
||||
|
||||
export default function useSettingsReviewDialog(dialog: DialogContextType): Promise<void>
|
||||
{
|
||||
return new Promise<void>(res =>
|
||||
{
|
||||
settingsForReview.getValue().then(needsReview =>
|
||||
{
|
||||
if (needsReview.length > 0)
|
||||
dialog.pushCustom(
|
||||
<SettingsReviewDialog />,
|
||||
undefined,
|
||||
() =>
|
||||
{
|
||||
settingsForReview.removeValue();
|
||||
res();
|
||||
}
|
||||
);
|
||||
else
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useSettingsReviewDialog } from "./hooks/useSettingsReviewDialog";
|
||||
@@ -0,0 +1 @@
|
||||
export { default as setSettingsReviewNeeded } from "./setSettingsReviewNeeded";
|
||||
@@ -0,0 +1,66 @@
|
||||
import { analyticsPermission } from "@/features/analytics";
|
||||
import { Runtime } from "wxt/browser";
|
||||
import { settingsForReview } from "./showSettingsReviewDialog";
|
||||
|
||||
export default async function setSettingsReviewNeeded(installReason: Runtime.OnInstalledReason, previousVersion?: string): Promise<void>
|
||||
{
|
||||
const needsReview: string[] = await settingsForReview.getValue();
|
||||
|
||||
if (!needsReview.includes(reviewSettings.ANALYTICS) && await checkAnalyticsReviewNeeded(installReason, previousVersion))
|
||||
needsReview.push(reviewSettings.ANALYTICS);
|
||||
|
||||
if (!needsReview.includes(reviewSettings.THUMBNAILS) && await checkThumbnailsReviewNeeded(installReason, previousVersion))
|
||||
needsReview.push(reviewSettings.THUMBNAILS);
|
||||
|
||||
console.log("Settings needing review:", needsReview);
|
||||
// Add more settings here as needed
|
||||
|
||||
if (needsReview.length > 0)
|
||||
await settingsForReview.setValue(needsReview);
|
||||
}
|
||||
|
||||
export const reviewSettings =
|
||||
{
|
||||
ANALYTICS: "analytics",
|
||||
THUMBNAILS: "thumbnails"
|
||||
};
|
||||
|
||||
async function checkAnalyticsReviewNeeded(installReason: Runtime.OnInstalledReason, previousVersion?: string): Promise<boolean>
|
||||
{
|
||||
if (installReason === "install")
|
||||
return !await analyticsPermission.getValue();
|
||||
|
||||
if (installReason === "update")
|
||||
{
|
||||
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt);
|
||||
const cumulative: number = major * 10000 + minor * 100 + patch;
|
||||
|
||||
if (cumulative < 30100) // < 3.1.0
|
||||
return true;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function checkThumbnailsReviewNeeded(installReason: Runtime.OnInstalledReason, previousVersion?: string): Promise<boolean>
|
||||
{
|
||||
if (installReason === "install")
|
||||
return true;
|
||||
|
||||
if (installReason === "update")
|
||||
{
|
||||
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt);
|
||||
const cumulative: number = major * 10000 + minor * 100 + patch;
|
||||
|
||||
if (cumulative < 30100) // < 3.1.0
|
||||
return true;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const settingsForReview = storage.defineItem<string[]>(
|
||||
"local:settingsForReview",
|
||||
{
|
||||
fallback: []
|
||||
}
|
||||
);
|
||||
@@ -1,17 +1,25 @@
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { DialogContextType } from "@/contexts/DialogProvider";
|
||||
import WelcomeDialog from "../components/WelcomeDialog";
|
||||
import { showWelcomeDialog } from "../utils/showWelcomeDialog";
|
||||
|
||||
export default function useWelcomeDialog(): void
|
||||
export default function useWelcomeDialog(dialog: DialogContextType): Promise<void>
|
||||
{
|
||||
const dialog = useDialog();
|
||||
|
||||
useEffect(() =>
|
||||
return new Promise<void>(res =>
|
||||
{
|
||||
showWelcomeDialog.getValue().then(showWelcome =>
|
||||
{
|
||||
if (showWelcome || import.meta.env.DEV)
|
||||
dialog.pushCustom(<WelcomeDialog />, undefined, () => showWelcomeDialog.removeValue());
|
||||
dialog.pushCustom(
|
||||
<WelcomeDialog />,
|
||||
undefined,
|
||||
() =>
|
||||
{
|
||||
showWelcomeDialog.removeValue();
|
||||
res();
|
||||
}
|
||||
);
|
||||
else
|
||||
res();
|
||||
});
|
||||
}, []);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user