Skip to content

Commit

Permalink
Widget: Personal Data (#217)
Browse files Browse the repository at this point in the history
* implementation of generic custom data

* builder test

* change wd for gha

* on pull-request test

* github action update

* no git checks with pnpm publisj

* npm auth token

* fix npm token

* login to npm

* pnpm login command

* branch release

* add tag to distinguish from latest

* renamed customData -> personalData, returning it in onRouteChange cbs

* tag with "dev"

* test changeset release

* release -> publish

* version first

* add changeset

* manual changeset

* rename snapshot release file

* regex serialization

* rename step Custom -> Personal Data

* snapshot release build widget

* add premade personalData fields into widget

* validate every field realtime after error

* eliminate circular import

* move personalData things to utils

* fix imports

* moved personalData code to utils

* disabled setting

* fix visual active step hard-coding

* enable specifying wallet connect step index

* small renaming

* fix issues stemming from WidgetCore

* added external validation

* revert/rename a bunch of things

* email field with alias

* removed logs

* move personal data step as first if there's required fields

* expose createWidgetTheme from widget

* fix theme export

* fix import

* fix error display

* remove console.logs

* use field.name instead of label

* allow longer domain extensions

* internal then external validation, external not running if internal fails

* remove validatePersonalData in builder

* unify event listeners and event callbacks as event handlers

* make callback non-required

* export alias field

* Fix up tests

* dist folder

* remve docs from monorepo

---------

Co-authored-by: Kaspar Kallas <[email protected]>
Co-authored-by: elvijsTDL <[email protected]>
  • Loading branch information
3 people authored Jan 26, 2024
1 parent f2f3e85 commit 0565290
Show file tree
Hide file tree
Showing 42 changed files with 818 additions and 292 deletions.
5 changes: 5 additions & 0 deletions .changeset/dirty-baboons-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@superfluid-finance/widget": patch
---

Add personal data handling to widget
5 changes: 5 additions & 0 deletions .changeset/thin-apples-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@superfluid-finance/widget": patch
---

Expose createWidgetTheme & WidgetThemeOptions
5 changes: 4 additions & 1 deletion .github/workflows/snapshot-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build Widget
run: pnpm build:widget

- name: Authenticate with NPM
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPMJS_TOKEN }}" > .npmrc

- name: Changeset version
run: pnpm changeset version --snapshot dev

- name: Changeset publish
run: pnpm changeset publish --tag dev
run: pnpm changeset publish --tag dev
9 changes: 6 additions & 3 deletions apps/hosted-widget/src/hooks/useLoadFromIPFS.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ThemeOptions } from "@mui/material";
import * as Sentry from "@sentry/nextjs";
import { PaymentDetails, ProductDetails } from "@superfluid-finance/widget";
import {
PaymentDetails,
ProductDetails,
WidgetThemeOptions,
} from "@superfluid-finance/widget";
import { useEffect, useState } from "react";

const gateway = "https://cloudflare-ipfs.com";
Expand All @@ -11,7 +14,7 @@ type ExportJSON = {
productDetails: ProductDetails;
paymentDetails: PaymentDetails;
layout: (typeof layoutTypes)[number];
theme: Omit<ThemeOptions, "unstable_strictMode" | "unstable_sxConfig">;
theme: WidgetThemeOptions;
};

type Result = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "@mui/material";
import {
paymentDetailsSchema,
personalDataSchema,
productDetailsSchema,
} from "@superfluid-finance/widget";
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
Expand All @@ -34,6 +35,7 @@ type ConfigEditorProps = {
const schema = z.object({
productDetails: productDetailsSchema,
paymentDetails: paymentDetailsSchema,
personalData: personalDataSchema,
type: z.enum(["dialog", "drawer", "full-screen", "page"]),
});

Expand Down Expand Up @@ -103,6 +105,7 @@ const ConfigEditor: FC<ConfigEditorProps> = ({ value, setValue }) => {
setValue("productDetails", parseResult.data.productDetails);
setValue("type", parseResult.data.type);
setValue("paymentDetails", parseResult.data.paymentDetails);
setValue("personalData", parseResult.data.personalData);
setSaved(true);
setTimeout(() => {
setSaved(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const NFTDeploymentDialog: FC<NFTDeploymentDialogProps> = ({
/>
<Stack sx={{ mb: 2 }}>
<Typography
data-testid="nft-success-message"
variant="h6"
textAlign="center"
>{`You deployed an NFT contract for ${successfulDeployments} networks.${
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh";
import { Box, Fab, Stack, TextField, Tooltip, Typography } from "@mui/material";
import { FC } from "react";
import {
Box,
Fab,
Stack,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import {
PersonalDataField,
PersonalDataFieldType,
} from "@superfluid-finance/widget/utils";
import { FC, useState } from "react";
import { Controller, useFieldArray, useFormContext } from "react-hook-form";

import useDemoMode from "../../hooks/useDemoMode";
Expand All @@ -11,14 +22,31 @@ import { WidgetProps } from "../widget-preview/WidgetPreview";
const ProductEditor: FC = () => {
const { control, watch } = useFormContext<WidgetProps>();

const [selectedPersonalDataFields, setSelectedPersonalDataFields] = useState<
Partial<Record<PersonalDataFieldType, boolean>>
>({});

watch(["paymentDetails.paymentOptions"]);
const { fields, append, remove } = useFieldArray({
control,
name: "paymentDetails.paymentOptions", // unique name for your Field Array
name: "personalData",
});

const [paymentOptions] = watch(["paymentDetails.paymentOptions"]);
const { setDemoProductDetails } = useDemoMode();

const onPersonalDataSelectionChange = (field: PersonalDataField) => {
const isFieldSelected =
selectedPersonalDataFields[field.name as PersonalDataFieldType];

const index = fields.findIndex(({ name }) => name === field.name);

isFieldSelected ? remove(index) : append(field);

setSelectedPersonalDataFields((prev) => ({
...prev,
[field.name]: !isFieldSelected,
}));
};

return (
<>
<Stack gap={2}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { colors, Fab, SelectChangeEvent, ThemeOptions } from "@mui/material";
import SuperfluidWidget, {
PaymentOption,
PersonalData,
ProductDetails,
WalletManager,
WidgetProps as WidgetProps_,
Expand Down Expand Up @@ -55,6 +56,7 @@ export type WidgetProps = {
paymentDetails: WidgetProps_["paymentDetails"] & {
paymentOptions: PaymentOption[];
};
personalData: PersonalData;
displaySettings: DisplaySettings;
type: Layout;
};
Expand All @@ -74,6 +76,7 @@ export const WidgetContext = createContext<WidgetProps>({
paymentOptions: [],
},
type: "dialog",
personalData: [],
displaySettings: {
stepperOrientation: "vertical",
darkMode: false,
Expand All @@ -95,6 +98,7 @@ const switchLayout = (
layout: Layout,
productDetails: ProductDetails,
paymentDetails: WidgetProps_["paymentDetails"],
personalData: WidgetProps_["personalData"],
theme: ThemeOptions,
walletManager: WalletManager,
stepperOrientation: "vertical" | "horizontal",
Expand All @@ -103,19 +107,40 @@ const switchLayout = (
<SuperfluidWidget
productDetails={productDetails}
paymentDetails={paymentDetails}
personalData={personalData}
tokenList={widgetBuilderTokenList}
type={layout}
theme={theme}
walletManager={walletManager}
stepper={{ orientation: stepperOrientation }}
eventListeners={{
onTransactionSent: console.log,
}}
eventListeners={
{
// onButtonClick: () => { console.log("onButtonClick eventListener") },
}
}
callbacks={
{
// onButtonClick: () => { console.log("onButtonClick callback") },
// validatePersonalData: async () => {
// return new Promise((resolve) => {
// setTimeout(() => {
// resolve({
// email: {
// success: false,
// message: "Async validation failed!",
// },
// });
// }, 1000);
// });
// },
}
}
/>
) : (
<SuperfluidWidget
productDetails={productDetails}
paymentDetails={paymentDetails}
personalData={personalData}
tokenList={widgetBuilderTokenList}
type={layout}
theme={theme}
Expand Down Expand Up @@ -178,7 +203,13 @@ export const mapDisplaySettingsToTheme = (
});

const WidgetPreview: FC<WidgetProps> = (props) => {
const { displaySettings, paymentDetails, productDetails, type } = props;
const {
displaySettings,
paymentDetails,
productDetails,
personalData,
type,
} = props;

const { open, isOpen, setDefaultChain } = useWeb3Modal();
const walletManager = useMemo<WalletManager>(
Expand Down Expand Up @@ -208,6 +239,7 @@ const WidgetPreview: FC<WidgetProps> = (props) => {
type,
productDetails,
paymentDetails,
personalData,
theme,
walletManager,
displaySettings.stepperOrientation,
Expand Down
1 change: 1 addition & 0 deletions apps/widget-builder/src/hooks/useDemoMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ const defaultDisplaySettings: DisplaySettings = {
export const defaultWidgetProps: WidgetProps = {
productDetails: defaultProductDetails,
paymentDetails: defaultPaymentDetails,
personalData: [],
type,
displaySettings: defaultDisplaySettings,
};
Expand Down
15 changes: 9 additions & 6 deletions apps/widget-builder/src/pages/builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,14 @@ export default function Builder() {

const { watch, control, getValues, setValue } = formMethods;

const [productDetails, paymentDetails, displaySettings, type] = watch([
"productDetails",
"paymentDetails",
"displaySettings",
"type",
]);
const [productDetails, paymentDetails, personalData, displaySettings, type] =
watch([
"productDetails",
"paymentDetails",
"personalData",
"displaySettings",
"type",
]);

const [isConfigEditorOpen, setConfigEditorOpen] = useState(false);

Expand Down Expand Up @@ -229,6 +231,7 @@ export default function Builder() {
{...{
productDetails,
paymentDetails,
personalData,
displaySettings,
type,
}}
Expand Down
9 changes: 6 additions & 3 deletions apps/widget-builder/src/types/export-json.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { ThemeOptions } from "@mui/material";
import { ProductDetails, WidgetProps } from "@superfluid-finance/widget";
import {
ProductDetails,
WidgetProps,
WidgetThemeOptions,
} from "@superfluid-finance/widget";

import { Layout } from "../components/widget-preview/WidgetPreview";

export type ExportJSON = {
productDetails: ProductDetails;
paymentDetails: WidgetProps["paymentDetails"];
type: Layout;
theme: Omit<ThemeOptions, "unstable_strictMode" | "unstable_sxConfig">;
theme: WidgetThemeOptions;
};
7 changes: 7 additions & 0 deletions packages/widget/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
"./metadata": {
"types": "./dist/index.metadata.d.ts",
"default": "./dist/index.metadata.js"
},
"./utils": {
"types": "./dist/utils.d.ts",
"default": "./dist/utils.js"
}
},
"main": "dist/index.js",
Expand All @@ -41,6 +45,9 @@
],
"metadata": [
"./dist/index.metadata.d.ts"
],
"utils": [
"./dist/utils.d.ts"
]
}
},
Expand Down
7 changes: 3 additions & 4 deletions packages/widget/src/AccountAddressCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { useCallback, useState } from "react";
import { Address } from "viem";

import { AccountAddress } from "./AccountAddress.js";
import { runEventListener } from "./EventListeners.js";
import { normalizeIcon } from "./helpers/normalizeIcon.js";
import { copyToClipboard } from "./utils.js";
import { useWidget } from "./WidgetContext.js";
Expand All @@ -37,18 +36,18 @@ export function AccountAddressCard({
}).toDataURL();
const [copied, setCopied] = useState(false);

const { eventListeners } = useWidget();
const { eventHandlers } = useWidget();
const onCopyAddressButtonClick = useCallback(
async (checksumAddress: string) => {
runEventListener(eventListeners.onButtonClick, {
eventHandlers.onButtonClick({
type: "switch_network",
});
await copyToClipboard(checksumAddress);
setCopied(true);
const timeoutId = setTimeout(() => setCopied(false), 1000);
return () => clearTimeout(timeoutId);
},
[eventListeners.onButtonClick],
[eventHandlers.onButtonClick],
);

return (
Expand Down
17 changes: 17 additions & 0 deletions packages/widget/src/Callbacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { EventListeners, PersonalData } from "./index.js";
import { Errors } from "./utils.js";

/**
* A set of blocking callback functions that are called at the appropriate time.
* @example
* <SuperfluidWidget callbacks={{
* validatePersonalData: async (props) => { fetch('https://example.com', { method: 'POST', body: props?.data }) }},
* }} />
*/

export interface Callbacks extends EventListeners {
/** Called when the user clicks the "Continue" button in the personal data step. */
validatePersonalData?: (
data: PersonalData,
) => Errors | void | Promise<Errors | void>;
}
10 changes: 6 additions & 4 deletions packages/widget/src/CheckoutConfig.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { ThemeOptions } from "@mui/material";
import { SuperTokenList } from "@superfluid-finance/tokenlist";
import { z } from "zod";

import { Callbacks } from "./Callbacks.js";
import {
NetworkAssets,
PaymentDetails,
paymentDetailsSchema,
ProductDetails,
productDetailsSchema,
} from "./core/index.js";
import { personalDataSchema } from "./core/PersonalData.js";
import { EventListeners } from "./EventListeners.js";
import { WidgetThemeOptions } from "./theme.js";
import { WalletManager } from "./WalletManager.js";

export const checkoutConfigSchema = z.object({
Expand Down Expand Up @@ -45,9 +47,8 @@ const widgetPropsSchema = z.object({
/**
* The MUI theme object to style the widget. Learn more about it from the MUI documentation: https://mui.com/material-ui/customization/default-theme/
*/
theme: z
.custom<Omit<ThemeOptions, "unstable_strictMode" | "unstable_sxConfig">>()
.optional(),
theme: z.custom<WidgetThemeOptions>().optional(),
personalData: personalDataSchema.optional(),
/**
* Whether the stepper UI component inside the widget is vertical or horizontal. Vertical is better supported.
*/
Expand All @@ -64,6 +65,7 @@ const widgetPropsSchema = z.object({
* @inheritdoc EventListeners
*/
eventListeners: z.custom<EventListeners>().optional(),
callbacks: z.custom<Callbacks>().optional(),
networkAssets: z.custom<NetworkAssets>().optional(),
});

Expand Down
Loading

3 comments on commit 0565290

@vercel
Copy link

@vercel vercel bot commented on 0565290 Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 0565290 Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 0565290 Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.