Files
TheGame/packages/web/lib/hooks/useSaveCeramicProfile.ts

231 lines
6.8 KiB
TypeScript

/* eslint-disable no-console */
import {
BasicProfile,
model as basicProfileModel,
} from '@datamodels/identity-profile-basic';
import { ModelManager } from '@glazed/devtools';
import { DIDDataStore } from '@glazed/did-datastore';
import { TileLoader } from '@glazed/tile-loader';
import {
AllProfileFields,
BasicProfileImages,
BasicProfileStrings,
ExtendedProfile,
ExtendedProfileImages,
extendedProfileModel,
ExtendedProfileObjects,
ExtendedProfileStrings,
HasuraEPObjects,
HasuraImageSourcedProps,
HasuraProfileProps,
HasuraStringProps,
Maybe,
Values,
} from '@metafam/utils';
import { useProfileField } from 'lib/hooks/useField';
import { useUser } from 'lib/hooks/useUser';
import { useWeb3 } from 'lib/hooks/useWeb3';
import { useRouter } from 'next/router';
import { ReactElement, useCallback } from 'react';
import { isEmpty } from 'utils/objectHelpers';
import { dispositionFor } from 'utils/playerHelpers';
export class CeramicError extends Error {
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor(message: string) {
super(message);
}
}
export const useSaveCeramicProfile = ({
debug = false,
setStatus = () => {},
fields = Object.keys(AllProfileFields),
}: {
debug?: boolean;
setStatus?: (msg?: Maybe<ReactElement | string>) => void;
fields?: Array<string>;
} = {}) => {
const router = useRouter();
const { ceramic } = useWeb3();
const { user } = useUser();
debug ||= !!router.query.debug; // eslint-disable-line no-param-reassign
const setters = Object.fromEntries(
fields.map((key) => {
const { setter = null } =
// eslint-disable-next-line react-hooks/rules-of-hooks
useProfileField({
field: key,
player: user,
owner: true,
});
return [key, setter];
}),
);
const save = useCallback(
async ({
values = {},
images = {},
}: {
values?: HasuraStringProps & HasuraEPObjects;
images?: HasuraImageSourcedProps;
}) => {
if (!ceramic) {
throw new CeramicError(
'Unable to connect to the Ceramic API to save changes.',
);
}
if (!ceramic.did?.authenticated) {
try {
setStatus('Authenticating DID…');
await ceramic.did?.authenticate();
} catch (err) {
if (
(err as Error).message ===
'Unexpected token u in JSON at position 0'
) {
throw new CeramicError('User canceled authentication.');
}
throw err;
}
}
const vals: HasuraProfileProps = { ...values };
const cache = new Map();
const loader = new TileLoader({ ceramic, cache });
const manager = new ModelManager(ceramic);
manager.addJSONModel(basicProfileModel);
manager.addJSONModel(extendedProfileModel);
const store = new DIDDataStore({
ceramic,
loader,
model: await manager.toPublished(),
});
// empty string fails validation
['countryCode', 'birthDate'].forEach((prop) => {
const key = prop as keyof typeof BasicProfileStrings;
if (vals[key] === '') {
delete vals[key];
}
});
const { countryCode: code }: { countryCode?: string } = vals;
if (code?.length === 2) {
vals.countryCode = code.toUpperCase();
} else if (code) {
throw new CeramicError(
`Country Code “${code}” is not the required two letters.`,
);
}
const basic: BasicProfile = {};
const extended: ExtendedProfile = {};
setStatus('Updating Basic Profile…');
Object.entries(BasicProfileStrings).forEach(([hasuraId, ceramicId]) => {
const fromKey = ceramicId as Values<typeof BasicProfileStrings>;
const toKey = hasuraId as keyof typeof BasicProfileStrings;
if (vals[toKey] !== undefined) {
basic[fromKey] = vals[toKey] ?? undefined;
}
});
Object.entries(BasicProfileImages).forEach(([hasuraId, ceramicId]) => {
const fromKey = ceramicId as Values<typeof BasicProfileImages>;
const toKey = hasuraId as keyof typeof BasicProfileImages;
if (images[toKey] !== undefined) {
basic[fromKey] = images[toKey] ?? undefined;
vals[toKey] = images[toKey]?.original.src;
}
});
if (!isEmpty(basic)) {
const basRes = await store.merge('basicProfile', basic);
if (debug) {
console.debug('Basic Profile:', basRes.toUrl());
}
}
setStatus('Updating Extended Profile…');
Object.entries(ExtendedProfileStrings).forEach(
([hasuraId, ceramicId]) => {
const fromKey = ceramicId as Values<typeof ExtendedProfileStrings>;
const toKey = hasuraId as keyof typeof ExtendedProfileStrings;
if (vals[toKey] !== undefined) {
extended[fromKey] = vals[toKey];
}
},
);
Object.entries(ExtendedProfileImages).forEach(([hasuraId, ceramicId]) => {
const fromKey = ceramicId as Values<typeof ExtendedProfileImages>;
const toKey = hasuraId as keyof typeof ExtendedProfileImages;
if (images[toKey] !== undefined) {
extended[fromKey] = images[toKey];
vals[toKey] = images[toKey]?.original.src;
}
});
Object.entries(ExtendedProfileObjects).forEach(
([hasuraId, ceramicId]) => {
const fromKey = ceramicId as Values<typeof ExtendedProfileObjects>;
const toKey = hasuraId as keyof typeof ExtendedProfileObjects;
if (vals[toKey] !== undefined) {
switch (fromKey) {
case 'availableHours': {
extended[fromKey] = vals[toKey] as number;
break;
}
case 'magicDisposition': {
extended[fromKey] = dispositionFor(vals.colorMask) ?? undefined;
break;
}
default: {
console.warn(`Unknown Profile Key: "${fromKey}"`);
}
}
}
},
);
if (debug) {
console.debug({ values, images, basic, extended });
}
if (!isEmpty(extended)) {
const extRes = await store.merge('extendedProfile', extended);
if (debug) {
console.debug('Extended Profile:', extRes.toUrl());
}
}
setStatus('Updating Local State…');
Object.entries(vals).forEach(([hasuraId, value]) => {
if (setters[hasuraId] != null) {
if (debug) {
console.debug(`Setting ${hasuraId} to “${value}”.`);
}
setters[hasuraId]?.(value);
} else {
console.warn(`Missing setter for ${hasuraId}.`, { setters });
}
});
return vals;
},
[ceramic, debug, setStatus, setters],
);
return save;
};