Files
TheGame/packages/web/lib/hooks/useSaveCeramicProfile.ts
δυς 317ba7c9e5 Refactor Setup Wizard & Profile Modal Configuration Panes (#1127)
* incorporating configuration panes from #1078

* standardizing player hero subcomponents & removing `owner` var from `useProfileField` 🚪

* switching box type, ambiguating overly-specific names, & massaging heights 📱

* reordering profile details sections, updating deployment environment, & conditionally wrapping the hero elements 🎬

* fixing render of color disposition selector 🕍

* self code review changes: removed some `;`s 🎋

* getting yarn typecheck && yarn lint to pass post rebase 🏇🏾

* handling "missing <div> in <button>" error with mounted check & setting HTTP return codes for OpenSea API endpoint 🕺

* `ProfileWizard` extends `Wizard`, roles display better on mobile, & pronouns use `ProfileWizardPane` 🍊

* properly encapsulating ETH address regex 🚲

* adding some tasks 🏥

* fixed skills layou

* handling default values? 

* corrections for revision comments from @dan13ram (UI bugs to follow) 🌋

* cleaning up memberships & explorer type 🧫

* refactoring player roles configuration and display to use `WizardPane` 🔮

* bunches of mobile fixes & repairing the display of deserialized skills 📟

* removing redirect in static props & formatting memberships display for responsiveness 🪆

* improving comprehensability of `/me` & more mobile tweaking 🍦

* various spacing fixes & a “try again” button for connecting 🫕

* maybe fixed issue with saving images for fields with default values 🇩🇿

* switched roles selection to a bounded `SimpleGrid` to fix sizing weirdness 🧰

* fix: removed verify on brightId button

* fix: clean up username page

* formatting & fixing skills issues 🥩

* reformatting NFT display 🚂

* adding `/join` 🚉

* style: peth's suggestions

* added mainnet required message

* style: slight tweak of megamenu item positions

* chore(release): 0.2.0

Co-authored-by: tenfinney <scott@1box.onmicrosoft.com>
Co-authored-by: dan13ram <dan13ram@gmail.com>
Co-authored-by: vidvidvid <weetopol@gmail.com>
2022-02-28 14:06:16 -05:00

230 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,
});
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;
};