New source found from dndbeyond.com
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
AbilityManager,
|
||||
FeaturesManager,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { AbilityManager } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { AttributesManagerContext } from "~/tools/js/Shared/managers/AttributesManagerContext";
|
||||
|
||||
@@ -18,7 +15,7 @@ export function useAbilities() {
|
||||
|
||||
setAbilities(abilities);
|
||||
}
|
||||
return FeaturesManager.subscribeToUpdates({ onUpdate });
|
||||
return AbilityManager.subscribeToUpdates({ onUpdate });
|
||||
}, [attributesManager, setAbilities]);
|
||||
|
||||
return abilities;
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
ApiAdapterUtils as apiAdapterUtils,
|
||||
BackgroundUtils as backgroundUtils,
|
||||
CampaignUtils as campaignUtils,
|
||||
CampaignSettingUtils as campaignSettingUtils,
|
||||
CharacterUtils as characterUtils,
|
||||
ChoiceUtils as choiceUtils,
|
||||
ClassFeatureUtils as classFeatureUtils,
|
||||
@@ -124,7 +123,6 @@ export const useCharacterEngine = () => ({
|
||||
baseFeats: useSelector(s.getBaseFeats),
|
||||
bonusSavingThrowModifiers: useSelector(s.getBonusSavingThrowModifiers),
|
||||
campaign: useSelector(s.getCampaign),
|
||||
campaignSettings: useSelector(sds.getCampaignSettings),
|
||||
carryingCapacity: useSelector(s.getCarryCapacity),
|
||||
castableSpellSlotLevels: useSelector(s.getCastableSpellSlotLevels),
|
||||
castablePactMagicSlotLevels: useSelector(s.getCastablePactMagicSlotLevels),
|
||||
@@ -151,7 +149,7 @@ export const useCharacterEngine = () => ({
|
||||
classSpells: useSelector(s.getClassSpells),
|
||||
classSpellInfo: useSelector(s.getClassSpellInfoLookup),
|
||||
classSpellLists: useSelector(s.getClassSpellLists),
|
||||
classSpellListSpells: useSelector(s.getClassSpellListSpellsLookup),
|
||||
classSpellListSpellsLookup: useSelector(s.getClassSpellListSpellsLookup),
|
||||
classes: useSelector(s.getClasses),
|
||||
classesModifiers: useSelector(s.getClassesModifiers),
|
||||
combinedMaxSpellSlotLevel: useSelector(s.getCombinedMaxSpellSlotLevel),
|
||||
@@ -170,6 +168,7 @@ export const useCharacterEngine = () => ({
|
||||
creatureOwnerData: useSelector(s.getCreatureOwnerData),
|
||||
creatureRules: useSelector(s.getCreatureRules),
|
||||
creatures: useSelector(s.getCreatures),
|
||||
currencies: useSelector(s.getCurrencies),
|
||||
currentCarriedWeightSpeed: useSelector(s.getCurrentCarriedWeightSpeed),
|
||||
currentCarriedWeightType: useSelector(s.getCurrentCarriedWeightType),
|
||||
currentLevel: useSelector(s.getCurrentLevel),
|
||||
@@ -235,6 +234,7 @@ export const useCharacterEngine = () => ({
|
||||
inventoryContainers: useSelector(s.getInventoryContainers),
|
||||
inventoryLookup: useSelector(s.getInventoryLookup),
|
||||
inventoryInfusion: useSelector(s.getInventoryInfusionLookup),
|
||||
itemPlans: useSelector(s.getAvailableItemPlans),
|
||||
isSheetReady: useSelector(s.isCharacterSheetReady),
|
||||
isDead: useSelector(s.isDead),
|
||||
isMulticlassCharacter: useSelector(s.isMulticlassCharacter),
|
||||
@@ -252,7 +252,9 @@ export const useCharacterEngine = () => ({
|
||||
levelSpells: useSelector(s.getLevelSpells),
|
||||
lifestyle: useSelector(s.getLifestyle),
|
||||
loadSpecies: useSelector(api.makeLoadAvailableRaces),
|
||||
longRestText: useSelector(sds.getLongRestText),
|
||||
maxPactMagicSlotLevel: useSelector(s.getMaxPactMagicSlotLevel),
|
||||
maxReplicatedItemsCount: useSelector(s.getMaxReplicatedItemsCount),
|
||||
maxSpellSlotLevel: useSelector(s.getMaxSpellSlotLevel),
|
||||
miscModifiers: useSelector(s.getMiscModifiers),
|
||||
modifierData: useSelector(s.getModifierData),
|
||||
@@ -279,7 +281,7 @@ export const useCharacterEngine = () => ({
|
||||
partyInfo: useSelector(sds.getPartyInfo),
|
||||
partyInventory: useSelector(s.getPartyInventory),
|
||||
partyInventoryContainers: useSelector(s.getPartyInventoryContainers),
|
||||
partyInventoryItem: useSelector(s.getPartyInventoryLookup),
|
||||
partyInventoryLookup: useSelector(s.getPartyInventoryLookup),
|
||||
passiveInsight: useSelector(s.getPassiveInsight),
|
||||
passiveInvestigation: useSelector(s.getPassiveInvestigation),
|
||||
passivePerception: useSelector(s.getPassivePerception),
|
||||
@@ -294,6 +296,7 @@ export const useCharacterEngine = () => ({
|
||||
playerName: useSelector(s.getUsername),
|
||||
preferences: useSelector(s.getCharacterPreferences),
|
||||
prerequisiteData: useSelector(s.getPrerequisiteData),
|
||||
premadeInfo: useSelector(char.getPremadeInfo),
|
||||
processedInitiative: useSelector(s.getProcessedInitiative),
|
||||
proficiency: useSelector(s.getProficiencyLookup),
|
||||
proficiencyBonus: useSelector(s.getProficiencyBonus),
|
||||
@@ -370,7 +373,6 @@ export const useCharacterEngine = () => ({
|
||||
apiAdapterUtils,
|
||||
backgroundUtils,
|
||||
campaignUtils,
|
||||
campaignSettingUtils,
|
||||
characterUtils,
|
||||
choiceUtils,
|
||||
classFeatureUtils,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { characterSelectors } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
||||
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { KETCH_RETRY_CONFIG } from "~/constants";
|
||||
import { KetchConsentResponse } from "~/types";
|
||||
|
||||
export const useConsent = () => {
|
||||
const setupAnalyticsConsentListener = useCallback(
|
||||
(onConsentChange: (hasConsent: boolean) => void) => {
|
||||
let retries = 0;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
onConsentChange(true);
|
||||
return () => {
|
||||
// noop cleanup for dev
|
||||
};
|
||||
}
|
||||
|
||||
const trySetup = () => {
|
||||
if ("window" in globalThis && window.ketch) {
|
||||
try {
|
||||
window.ketch(
|
||||
"on",
|
||||
"consent",
|
||||
(consentData: KetchConsentResponse) => {
|
||||
onConsentChange(Boolean(consentData.purposes?.analytics));
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to setup Ketch consent listener:", error);
|
||||
onConsentChange(false);
|
||||
}
|
||||
} else if (retries < KETCH_RETRY_CONFIG.maxRetries) {
|
||||
retries++;
|
||||
timeoutId = setTimeout(trySetup, KETCH_RETRY_CONFIG.retryInterval);
|
||||
} else {
|
||||
console.warn(
|
||||
`Failed to initialize consent management after ${KETCH_RETRY_CONFIG.maxRetries} retries`
|
||||
);
|
||||
onConsentChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
trySetup();
|
||||
|
||||
// Return cleanup function to cancel pending retries
|
||||
return () => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
setupAnalyticsConsentListener,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { datadogRum } from "@datadog/browser-rum";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { User } from "@dndbeyond/authentication-lib-js";
|
||||
|
||||
import config from "~/config";
|
||||
import { useConsent } from "~/hooks/useConsent";
|
||||
|
||||
const isDndbeyondUrl = (url: string): boolean => {
|
||||
try {
|
||||
const { protocol, hostname } = new URL(url);
|
||||
return (
|
||||
protocol === "https:" &&
|
||||
(hostname === "dndbeyond.com" || hostname.endsWith(".dndbeyond.com"))
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const useDatadogRum = (user: User | null) => {
|
||||
const [hasConsent, setHasConsent] = useState(false);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const environment = config.environment;
|
||||
const version = config.version.replace(/@dndbeyond\/character-app@/, ""); // example: 1.70.116
|
||||
const { setupAnalyticsConsentListener } = useConsent();
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = setupAnalyticsConsentListener(setHasConsent);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [setupAnalyticsConsentListener]);
|
||||
|
||||
useEffect(() => {
|
||||
if (datadogRum.getInternalContext()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
datadogRum.init({
|
||||
applicationId: "2cfa3248-a632-4f5b-bf9e-aafa18dc69ae", // Not a secret, safe to include in client code
|
||||
clientToken: "pub940bbbc614a4ae3ab0c872fdad597554", // Not a secret, safe to include in client code
|
||||
site: "datadoghq.com",
|
||||
service: "characters-app",
|
||||
env: environment === "production" ? "live" : environment,
|
||||
version: `${version}`,
|
||||
sessionSampleRate: environment === "production" ? 10 : 100,
|
||||
sessionReplaySampleRate: environment === "production" ? 1 : 10,
|
||||
traceContextInjection: "sampled",
|
||||
traceSampleRate: 10,
|
||||
trackingConsent: "not-granted", // Start with no consent, will update when consent status is known
|
||||
trackUserInteractions: true,
|
||||
trackResources: true,
|
||||
trackLongTasks: true,
|
||||
defaultPrivacyLevel: "mask-user-input",
|
||||
allowedTracingUrls: [isDndbeyondUrl],
|
||||
});
|
||||
|
||||
setIsInitialized(true);
|
||||
} catch (error) {
|
||||
console.warn("Datadog RUM failed to initialize:", error);
|
||||
}
|
||||
}, [environment]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
datadogRum.clearUser();
|
||||
return;
|
||||
}
|
||||
|
||||
datadogRum.setUser({
|
||||
id: `${user.id ?? ""}`,
|
||||
name: `${user.name ?? ""}`,
|
||||
sub_tier: `${user.subscriptionTier ?? ""}`,
|
||||
});
|
||||
}, [user, isInitialized]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
datadogRum.setTrackingConsent(hasConsent ? "granted" : "not-granted");
|
||||
}, [hasConsent, isInitialized]);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useDispatch as useReactReduxDispatch } from "react-redux";
|
||||
import { AnyAction, Dispatch } from "redux";
|
||||
|
||||
// TODO: We are typing useDispatch this way to work around issues with UnknownAction, the type that was introduced in Redux 5.
|
||||
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
|
||||
export const useDispatch =
|
||||
useReactReduxDispatch.withTypes<Dispatch<AnyAction>>();
|
||||
@@ -1,33 +1,34 @@
|
||||
import { HTMLAttributes, useState } from "react";
|
||||
import styles from '../../styles/errors.module.css';
|
||||
import { HTMLAttributes, JSX, useState } from "react";
|
||||
|
||||
import styles from "../../styles/errors.module.css";
|
||||
|
||||
export interface ErrorHandlerOptions {
|
||||
initialState: boolean;
|
||||
errMsg: string;
|
||||
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>;
|
||||
};
|
||||
initialState: boolean;
|
||||
errMsg: string;
|
||||
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export interface ErrorHandler {
|
||||
showError: boolean;
|
||||
setShowError: (value: boolean) => void;
|
||||
ErrorMessage: () => JSX.Element;
|
||||
};
|
||||
showError: boolean;
|
||||
setShowError: (value: boolean) => void;
|
||||
ErrorMessage: () => JSX.Element;
|
||||
}
|
||||
|
||||
export const useErrorHandling = (
|
||||
initialState: boolean,
|
||||
errMsg: string,
|
||||
errorMessageAttributes: HTMLAttributes<HTMLDivElement> | null = null
|
||||
initialState: boolean,
|
||||
errMsg: string,
|
||||
errorMessageAttributes: HTMLAttributes<HTMLDivElement> | null = null
|
||||
): ErrorHandler => {
|
||||
const [showError, setShowError] = useState(initialState);
|
||||
const ErrorMessage = (): JSX.Element => (
|
||||
<div className={styles.inputError} {...errorMessageAttributes}>
|
||||
{errMsg}
|
||||
</div>
|
||||
);
|
||||
const [showError, setShowError] = useState(initialState);
|
||||
const ErrorMessage = (): JSX.Element => (
|
||||
<div className={styles.inputError} {...errorMessageAttributes}>
|
||||
{errMsg}
|
||||
</div>
|
||||
);
|
||||
|
||||
return {
|
||||
showError,
|
||||
setShowError,
|
||||
ErrorMessage,
|
||||
};
|
||||
return {
|
||||
showError,
|
||||
setShowError,
|
||||
ErrorMessage,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import { HTMLAttributes } from "react";
|
||||
import { HTMLAttributes, JSX } from "react";
|
||||
|
||||
import { useErrorHandling } from "./useErrorHandling";
|
||||
|
||||
export interface MaxLengthErrorHandler {
|
||||
handleMaxLengthErrorMsg: (value: string) => void;
|
||||
hideError: () => void;
|
||||
MaxLengthErrorMessage: () => JSX.Element;
|
||||
};
|
||||
handleMaxLengthErrorMsg: (value: string) => void;
|
||||
hideError: () => void;
|
||||
MaxLengthErrorMessage: () => JSX.Element;
|
||||
}
|
||||
|
||||
export const useMaxLengthErrorHandling = (
|
||||
initialState: boolean,
|
||||
maxLength: number | null,
|
||||
errMsg: string = "",
|
||||
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>
|
||||
initialState: boolean,
|
||||
maxLength: number | null,
|
||||
errMsg: string = "",
|
||||
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>
|
||||
): MaxLengthErrorHandler => {
|
||||
errMsg ||= `The max length is ${maxLength} characters.`;
|
||||
errMsg ||= `The max length is ${maxLength} characters.`;
|
||||
|
||||
const {
|
||||
showError,
|
||||
setShowError,
|
||||
ErrorMessage,
|
||||
} = useErrorHandling(initialState, errMsg, errorMessageAttributes);
|
||||
const { showError, setShowError, ErrorMessage } = useErrorHandling(
|
||||
initialState,
|
||||
errMsg,
|
||||
errorMessageAttributes
|
||||
);
|
||||
|
||||
const handleMaxLengthErrorMsg = (value: string): void => {
|
||||
if (!maxLength) {
|
||||
// Skip if maxLength is 0 or not set.
|
||||
return;
|
||||
}
|
||||
|
||||
const isTooLong = (value?.length ?? 0) >= (maxLength ?? 0);
|
||||
if (isTooLong !== showError) {
|
||||
setShowError(isTooLong);
|
||||
}
|
||||
};
|
||||
|
||||
const hideError = () => {
|
||||
setShowError(false);
|
||||
const handleMaxLengthErrorMsg = (value: string): void => {
|
||||
if (!maxLength) {
|
||||
// Skip if maxLength is 0 or not set.
|
||||
return;
|
||||
}
|
||||
|
||||
const MaxLengthErrorMessage = (): JSX.Element => showError && errMsg
|
||||
? (<ErrorMessage />)
|
||||
: <></>;
|
||||
const isTooLong = (value?.length ?? 0) >= (maxLength ?? 0);
|
||||
if (isTooLong !== showError) {
|
||||
setShowError(isTooLong);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleMaxLengthErrorMsg,
|
||||
MaxLengthErrorMessage,
|
||||
hideError
|
||||
};
|
||||
}
|
||||
const hideError = () => {
|
||||
setShowError(false);
|
||||
};
|
||||
|
||||
const MaxLengthErrorMessage = (): JSX.Element =>
|
||||
showError && errMsg ? <ErrorMessage /> : <></>;
|
||||
|
||||
return {
|
||||
handleMaxLengthErrorMsg,
|
||||
MaxLengthErrorMessage,
|
||||
hideError,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
ExtraManager,
|
||||
FeaturesManager,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { ExtraManager } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { ExtrasManagerContext } from "~/tools/js/Shared/managers/ExtrasManagerContext";
|
||||
|
||||
@@ -19,7 +16,7 @@ export function useExtras() {
|
||||
const onUpdate = () => {
|
||||
setExtras(extrasManager.getCharacterExtraManagers());
|
||||
};
|
||||
return FeaturesManager.subscribeToUpdates({ onUpdate });
|
||||
return ExtraManager.subscribeToUpdates({ onUpdate });
|
||||
}, [extrasManager]);
|
||||
|
||||
return extras;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
ApiAdapterUtils,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
syncTransactionActions,
|
||||
} from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
||||
|
||||
export interface ExportPdfData {
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface AppUser extends User {
|
||||
roles: Array<string>;
|
||||
subscription: string;
|
||||
subscriptionTier: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
/**
|
||||
* AppUserState has three states:
|
||||
|
||||
Reference in New Issue
Block a user