Skip to content

Commit

Permalink
Frictionless flow updates
Browse files Browse the repository at this point in the history
  • Loading branch information
HughParry committed Nov 25, 2024
1 parent 6247a15 commit 66b5012
Show file tree
Hide file tree
Showing 14 changed files with 113 additions and 98 deletions.
9 changes: 0 additions & 9 deletions demos/client-bundle-example/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,6 @@ <h1>Example Login Form</h1>
<div class="mui-textfield mui-textfield--float-label">
<div id="procaptcha-container"></div>
</div>
<div class="mui-textfield mui-textfield--float-label">
<!-- Dev sitekey -->
<div
class="procaptcha"
data-theme="light"
data-sitekey="%PROSOPO_SITE_KEY%"
data-failed-callback="onCaptchaFailed"
></div>
</div>
<div id="messageContainer" style="display: none; color: black;"><span id="message"></span></div>
<button type="button" class="mui-btn mui-btn--raised" onclick="onActionHandler()" data-cy="submit-button">Submit</button>
</form>
Expand Down
1 change: 1 addition & 0 deletions packages/procaptcha-bundle/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ declare global {
const start = () => {
// onLoadUrlCallback defines the name of the callback function to be called when the script is loaded
// onRenderExplicit takes values of either explicit or implicit
console.log("Starting Procaptcha");
const { onloadUrlCallback, renderExplicit } = extractParams(BUNDLE_NAME);

// Render the Procaptcha component implicitly if renderExplicit is not set to explicit
Expand Down
15 changes: 13 additions & 2 deletions packages/procaptcha-bundle/src/util/defaultCallbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type ProcaptchaToken,
} from "@prosopo/types";
import { getParentForm } from "./form.js";
import { reset } from "../index.js";

export const getWindowCallback = (callbackName: string) => {
// biome-ignore lint/suspicious/noExplicitAny: TODO fix any
Expand Down Expand Up @@ -54,6 +55,7 @@ export const getDefaultCallbacks = (element: Element) => ({
},
onReset: () => {
removeProcaptchaResponse();
reset();
console.log("Captcha widget reset");
},
});
Expand Down Expand Up @@ -197,14 +199,23 @@ export function setUserCallbacks(
// reset callback
if (renderOptions?.["reset-callback"]) {
if (typeof renderOptions["reset-callback"] === "function") {
callbacks.onReset = renderOptions["reset-callback"];
const fn = renderOptions["reset-callback"];
callbacks.onReset = () => {
removeProcaptchaResponse();
reset();
fn();
};
} else {
const onResetCallbackName =
typeof renderOptions?.["reset-callback"] === "string"
? renderOptions?.["reset-callback"]
: element.getAttribute("data-reset-callback");
if (onResetCallbackName)
callbacks.onReset = getWindowCallback(onResetCallbackName);
callbacks.onReset = () => {
removeProcaptchaResponse();
reset();
getWindowCallback(onResetCallbackName)();
};
}
}
}
Expand Down
88 changes: 52 additions & 36 deletions packages/procaptcha-frictionless/src/ProcaptchaFrictionless.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,50 +67,66 @@ export const ProcaptchaFrictionless = ({
callbacks,
detectBot = customDetectBot,
}: ProcaptchaFrictionlessProps) => {
const [componentToRender, setComponentToRender] = useState(
<ProcaptchaPlaceholder config={config} callbacks={callbacks} />,
);
const [frictionlessLoading, setFrictionlessLoading] = useState(false);
const configOutput = ProcaptchaConfigSchema.parse(config);

useEffect(() => {
const configOutput = ProcaptchaConfigSchema.parse(config);
const frictionlessConfig = {
...config,
startOnRender: true,
};

const detectAndSetComponent = async () => {
try {
const result = await detectBot(configOutput);
const detectAndSetComponent = async () => {
setComponentToRender(
<ProcaptchaPlaceholder
config={config}
callbacks={callbacks}
frictionlessLoading={true}
detectAndSetComponent={() => {}}
/>,
);

const frictionlessState: FrictionlessState = {
provider: result.provider,
sessionId: result.sessionId,
userAccount: result.userAccount,
};
try {
const result = await detectBot(configOutput);

if (result.captchaType === "image") {
setComponentToRender(
<Procaptcha
config={config}
callbacks={callbacks}
frictionlessState={frictionlessState}
/>,
);
} else {
setComponentToRender(
<ProcaptchaPow
config={config}
callbacks={callbacks}
frictionlessState={frictionlessState}
/>,
);
}
} catch (error) {
console.error(error);
const frictionlessState: FrictionlessState = {
provider: result.provider,
sessionId: result.sessionId,
userAccount: result.userAccount,
};

if (result.captchaType === "image") {
setComponentToRender(
<Procaptcha config={config} callbacks={callbacks} />,
<Procaptcha
config={frictionlessConfig}
callbacks={callbacks}
frictionlessState={frictionlessState}
/>,
);
} else {
setComponentToRender(
<ProcaptchaPow
config={frictionlessConfig}
callbacks={callbacks}
frictionlessState={frictionlessState}
/>,
);
}
};
} catch (error) {
console.error(error);
setComponentToRender(
<Procaptcha config={frictionlessConfig} callbacks={callbacks} />,
);
}
};

detectAndSetComponent();
}, [config, callbacks, detectBot, config.language]);
const [componentToRender, setComponentToRender] = useState(
<ProcaptchaPlaceholder
config={config}
callbacks={callbacks}
frictionlessLoading={false}
detectAndSetComponent={detectAndSetComponent}
/>,
);

return componentToRender;
};
2 changes: 2 additions & 0 deletions packages/procaptcha-pow/src/components/ProcaptchaPoW.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export const ProcaptchaPow = (props: ProcaptchaProps) => (
<ProcaptchaPlaceholder
config={props.config}
callbacks={props.callbacks}
frictionlessLoading={true}
detectAndSetComponent={() => {}}
/>
}
>
Expand Down
5 changes: 5 additions & 0 deletions packages/procaptcha-pow/src/components/ProcaptchaWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ const Procaptcha = (props: ProcaptchaProps) => {
}
}, [config.language]);

if (config.startOnRender) {
manager.current.start();
config.startOnRender = false;
}

return (
<div ref={captchaRef}>
<div
Expand Down
2 changes: 2 additions & 0 deletions packages/procaptcha-pow/src/services/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ export const Manager = (
loading: false,
error: challenge.error.message,
});
events.onFailed();
callbacks?.onReset?.();
} else {
const solution = solvePoW(challenge.challenge, challenge.difficulty);

Expand Down
2 changes: 2 additions & 0 deletions packages/procaptcha-react/src/components/Procaptcha.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const Procaptcha = (props: ProcaptchaProps) => (
<ProcaptchaPlaceholder
config={props.config}
callbacks={props.callbacks}
frictionlessLoading={false}
detectAndSetComponent={() => {}}
/>
}
>
Expand Down
5 changes: 5 additions & 0 deletions packages/procaptcha-react/src/components/ProcaptchaWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ const ProcaptchaWidget = (props: ProcaptchaProps) => {
}
}, [config.language]);

if (config.startOnRender) {
manager.start();
config.startOnRender = false;
}

return (
<div>
<div
Expand Down
47 changes: 2 additions & 45 deletions packages/procaptcha/src/modules/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ const defaultState = (): Partial<ProcaptchaState> => {
isHuman: false,
captchaApi: undefined,
account: undefined,
// don't handle timeout here, this should be handled by the state management
};
};

Expand Down Expand Up @@ -112,7 +111,6 @@ export function Manager(
}
await cryptoWaitReady();

resetState();
// set the loading flag to true (allow UI to show some sort of loading / pending indicator while we get the captcha process going)
updateState({ loading: true });
updateState({
Expand Down Expand Up @@ -158,26 +156,12 @@ export function Manager(
throw new ProsopoDatasetError("DEVELOPER.PROVIDER_NO_CAPTCHA");
}

// setup timeout, taking the timeout from the individual captcha or the global default
const timeMillis: number = challenge.captchas
.map(
(captcha) =>
captcha.timeLimitMs || config.captchas.image.challengeTimeout,
)
.reduce((a: number, b: number) => a + b);
const timeout = setTimeout(() => {
events.onChallengeExpired();
// expired, disallow user's claim to be human
updateState({ isHuman: false, showModal: false, loading: false });
}, timeMillis);

// update state with new challenge
updateState({
index: 0,
solutions: challenge.captchas.map(() => []),
challenge,
showModal: true,
timeout,
});
}
},
Expand All @@ -191,9 +175,6 @@ export function Manager(
const submit = async () => {
await providerRetry(
async () => {
// disable the time limit, user has submitted their solution in time
clearTimeout();

if (!state.challenge) {
throw new ProsopoError("CAPTCHA.NO_CAPTCHA", {
context: { error: "Cannot submit, no Captcha found in state" },
Expand Down Expand Up @@ -296,7 +277,6 @@ export function Manager(
},
}),
);
setValidChallengeTimeout();
} else {
events.onFailed();
}
Expand All @@ -309,17 +289,15 @@ export function Manager(
};

const cancel = async () => {
// disable the time limit
clearTimeout();
// abandon the captcha process
resetState();
// trigger the onClose event
events.onClose();
// trigger full reset
callbacks?.onReset?.();
};

const reload = async () => {
// disable the time limit
clearTimeout();
// abandon the captcha process
resetState();
// trigger the onClose event
Expand Down Expand Up @@ -386,28 +364,7 @@ export function Manager(
return new ProviderApi(providerUrl, config.account.address);
};

const clearTimeout = () => {
// clear the timeout
window.clearTimeout(Number(state.timeout));
// then clear the timeout from the state
updateState({ timeout: undefined });
};

const setValidChallengeTimeout = () => {
const timeMillis: number = configOptional.captchas.image.solutionTimeout;
const successfullChallengeTimeout = setTimeout(() => {
// Human state expired, disallow user's claim to be human
updateState({ isHuman: false });

events.onExpired();
}, timeMillis);

updateState({ successfullChallengeTimeout });
};

const resetState = () => {
// clear timeout just in case a timer is still active (shouldn't be)
clearTimeout();
updateState(defaultState());
events.onReset();
};
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ export const ProcaptchaConfigSchema = ProsopoClientConfigSchema.and(
theme: ThemeType.optional().default("light"),
captchas: CaptchaTimeoutSchema.optional().default(defaultCaptchaTimeouts),
language: LanguageSchema.optional(),
startOnRender: boolean().optional().default(false),
}),
);

Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/procaptcha/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ export interface ProcaptchaProps {
callbacks?: Partial<ProcaptchaCallbacks>;
frictionlessState?: FrictionlessState;
}

export interface ProcaptchaPlaceholderProps extends ProcaptchaProps {
detectAndSetComponent: () => void;
frictionlessLoading: boolean;
}
2 changes: 2 additions & 0 deletions packages/web-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
},
"devDependencies": {
"@prosopo/config": "2.1.14",
"@prosopo/locale-browser": "2.1.14",
"@prosopo/types": "2.1.14",
"@vitest/coverage-v8": "2.1.1",
"concurrently": "9.0.1",
"del-cli": "6.0.0",
Expand Down
Loading

0 comments on commit 66b5012

Please sign in to comment.