Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: have one hcaptcha provider #75

Merged
merged 1 commit into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/app/[locale]/faucet/_components/FaucetRequestButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { useAccount } from "wagmi";

import { Backdrop } from "@/components/Backdrop";
import { ColoredText } from "@/components/ColoredText";
import { useCaptcha } from "@/contexts/CaptchaProvider";
import { clientEnv } from "@/env-client";
import { useFaucetInfoAndCaptcha } from "@/hooks/useFaucetInfoAndHCaptcha";

interface FaucetRequestButtonProps {
onClick?: () => void;
Expand Down Expand Up @@ -140,7 +140,7 @@ export const FaucetRequestButton: React.FC<FaucetRequestButtonProps> = ({
const [requestLoading, setRequestLoading] = useState(false);
const [waitingForConnection, setWaitingForConnection] = useState(false);
const hasAutoRequested = useRef(false);
const { hcaptchaLoaded, executeHCaptcha } = useFaucetInfoAndCaptcha(chainId);
const { isReady, executeHCaptcha } = useCaptcha();
const { isConnected } = useAccount();
const { openConnectModal } = useConnectModal();
const [tweetUrl, setTweetUrl] = useState("");
Expand Down Expand Up @@ -277,7 +277,7 @@ export const FaucetRequestButton: React.FC<FaucetRequestButtonProps> = ({

// If not rate limited, proceed with captcha verification
let hcaptchaToken = undefined;
if (hcaptchaLoaded) {
if (isReady) {
try {
setIsHCaptchaVisible(true);

Expand Down Expand Up @@ -351,7 +351,7 @@ export const FaucetRequestButton: React.FC<FaucetRequestButtonProps> = ({
isConnected,
openConnectModal,
onChange,
hcaptchaLoaded,
isReady,
executeHCaptcha,
tweetUrl,
skipTweetPrompt,
Expand Down
13 changes: 0 additions & 13 deletions src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,6 @@ export default async function LocaleLayout({
/>

<body>
<div
id="hcaptcha-container"
style={{
display: "none",
position: "fixed",
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: 9999,
overflow: "hidden",
}}
/>
<NextIntlClientProvider locale={locale} messages={messages}>
<Providers>
<ToggleThemeShortcut />
Expand Down
6 changes: 4 additions & 2 deletions src/app/[locale]/testnet-bridge/_components/Bridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export function Bridge() {
const renderNetworkError = () => {
if (networkError) {
return (
<div className="text-red-500 text-sm mt-2 text-center">
<div className="ink:text-status-error text-sm mt-2 text-center">
{networkError}
</div>
);
Expand Down Expand Up @@ -482,7 +482,9 @@ export function Bridge() {
)}
</div>
{error && (
<span className="text-red-500 text-sm">{error}</span>
<span className="ink:text-status-error text-sm">
{error}
</span>
)}
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/ConnectWalletButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const ConnectWalletButton: React.FC<ConnectWalletButtonProps> = ({
}
className={classNames(
chain.unsupported &&
"bg-red-500/15 hover:bg-red-500/25 dark:text-red-400",
"ink:text-status-error ink:bg-status-error-bg",
shrinkOnMobile ? "sm:hidden" : "hidden",
className
)}
Expand All @@ -94,7 +94,7 @@ export const ConnectWalletButton: React.FC<ConnectWalletButtonProps> = ({
}
className={classNames(
chain.unsupported &&
"bg-red-500/15 hover:bg-red-500/25 dark:text-red-400",
"ink:text-status-error ink:bg-status-error-bg",
shrinkOnMobile ? "hidden sm:block" : "",
className
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ContactUsPrivacyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import React from "react";
import { Button, useModalContext } from "@inkonchain/ink-kit";
import { useModalContext } from "@inkonchain/ink-kit";

import { CONTACT_US_MODAL_KEY } from "./Modals/ContactUsModal";

Expand Down
2 changes: 1 addition & 1 deletion src/components/FormStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const FormStatus: React.FC<FormStatusProps> = ({
<TemporaryMessage timeout={3000}>
<div className="flex items-center gap-3 py-3 px-4 ink:text-status-error ink:bg-status-error rounded-full w-full">
<WarningTriangleIcon size="icon-md" enforce="inherit" />
<p className="text-base font-medium">{errorMessage}</p>
<div className="text-base font-medium">{errorMessage}</div>
</div>
</TemporaryMessage>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
CategoryValue,
TagValue,
} from "@/schemas/app-submission-schema";
import { classNames } from "@/util/classes";

interface AppSubmissionFormProps {
form: UseFormReturn<z.infer<typeof appSubmissionSchema>>;
Expand All @@ -42,10 +41,10 @@ export const AppSubmissionForm: React.FC<AppSubmissionFormProps> = ({
return (
<FormProvider {...form}>
{state?.message !== "" && !state.issues && (
<div className="text-red-500">{state.message}</div>
<div className="ink:text-status-error">{state.message}</div>
)}
{state?.issues && (
<div className="text-red-500">
<div className="ink:text-status-error">
<ul>
{state.issues.map((issue, index) => (
<li key={`error-${index}`} className="flex gap-1">
Expand Down Expand Up @@ -189,18 +188,16 @@ export const AppSubmissionForm: React.FC<AppSubmissionFormProps> = ({
);
};

const SubmitButton: React.FC<{ isSubmitting: boolean }> = ({
isSubmitting,
}) => (
const SubmitButton: React.FC<{
isSubmitting: boolean;
}> = ({ isSubmitting }) => (
// Not using "disabled" property as it seems to mess up the scroll position when submitting...
// TODO: Figure out root cause of this weird behavior
<Button
variant="primary"
size="md"
type="submit"
className={classNames({
"opacity-60 cursor-not-allowed hover:cursor-not-allowed": isSubmitting,
})}
disabled={isSubmitting}
onClick={(e) => {
if (isSubmitting) {
e.preventDefault();
Expand Down Expand Up @@ -244,7 +241,7 @@ const InputField: React.FC<InputFieldProps> = ({
<div className="flex flex-col gap-1">
<label className="font-semibold" htmlFor={name}>
{label}
{required && <span className="text-red-500 ml-0.5">*</span>}
{required && <span className="ink:text-status-error ml-0.5">*</span>}
</label>
<Input
placeholder={placeholder}
Expand Down Expand Up @@ -300,7 +297,7 @@ const FileInput: React.FC<Omit<InputFieldProps, "placeholder">> = ({
<div className="flex flex-col gap-1">
<label className="font-semibold" htmlFor={name}>
{label}
{required && <span className="text-red-500 ml-0.5">*</span>}
{required && <span className="ink:text-status-error ml-0.5">*</span>}
</label>
<div className="flex flex-col items-start">
{preview ? (
Expand Down Expand Up @@ -386,7 +383,7 @@ const SelectField = <T extends string>({
<div className="flex flex-col gap-1">
<label className="font-semibold" htmlFor={name}>
{label}
{required && <span className="text-red-500 ml-0.5">*</span>}
{required && <span className="ink:text-status-error ml-0.5">*</span>}
</label>
<input type="hidden" {...register(name)} value={selected.value} />
<Listbox
Expand Down Expand Up @@ -429,7 +426,7 @@ const MultiSelectField = <T extends string>({
<div className="flex flex-col gap-1">
<label className="font-semibold" htmlFor={name}>
{label}
{required && <span className="text-red-500 ml-0.5">*</span>}
{required && <span className="ink:text-status-error ml-0.5">*</span>}
</label>
<Listbox
multiple
Expand Down
5 changes: 4 additions & 1 deletion src/components/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ModalProvider } from "@inkonchain/ink-kit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

import { AnalyticsProvider } from "@/contexts/AnalyticsProvider";
import { CaptchaProvider } from "@/contexts/CaptchaProvider";
import { RelayProvider } from "@/contexts/RelayProvider";
import { ThemeProvider } from "@/contexts/ThemeProvider";
import { WalletProvider } from "@/contexts/WalletProvider";
Expand All @@ -21,7 +22,9 @@ export const Providers: React.FC<PropsWithChildren> = ({ children }) => {
<AnalyticsProvider writeKey={clientEnv.NEXT_PUBLIC_SEGMENT_WRITE_KEY}>
<QueryClientProvider client={queryClient}>
<WalletProvider>
<RelayProvider>{children}</RelayProvider>
<RelayProvider>
<CaptchaProvider>{children}</CaptchaProvider>
</RelayProvider>
</WalletProvider>
</QueryClientProvider>
</AnalyticsProvider>
Expand Down
129 changes: 129 additions & 0 deletions src/contexts/CaptchaProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"use client";

import React, {
createContext,
PropsWithChildren,
ReactNode,
useCallback,
useContext,
useState,
} from "react";
import Script from "next/script";

import { clientEnv } from "@/env-client";

declare global {
interface Window {
hcaptcha: {
render: (
containerId: string,
options: {
sitekey: string;
size?: string;
callback?: (token: string) => void;
}
) => string;
execute: (
widgetId?: string,
options?: { async: boolean }
) => Promise<{ response: string }>;
reset: (widgetId?: string) => void;
};
}
}

async function executeHCaptcha(widgetId: string) {
if (!widgetId) throw new Error("hCaptcha not initialized");
try {
console.debug("Executing hCaptcha with widgetId:", widgetId);
const token = await window.hcaptcha.execute(widgetId, { async: true });
return token;
} catch (error) {
console.error("hCaptcha execution error:", error);
throw error;
}
}

interface CaptchaContextType {
executeHCaptcha: () => Promise<{ response: string }>;
isReady: boolean;
isLoading: boolean;
error: Error | null;
}

const CaptchaContext = createContext<CaptchaContextType | undefined>(undefined);

export const useCaptcha = (): CaptchaContextType => {
const context = useContext(CaptchaContext);
if (!context) {
throw new Error("useCaptcha must be used within a CaptchaProvider");
}
return context;
};

export const CaptchaProvider: React.FC<PropsWithChildren> = ({ children }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [widgetId, setWidgetId] = useState<string | null>(null);

function init() {
const id = window.hcaptcha.render("hcaptcha-container", {
sitekey: clientEnv.NEXT_PUBLIC_HCAPTCHA_SITEKEY,
size: "invisible",
});
setWidgetId(id);
}

const handleExecuteHCaptcha = useCallback(async () => {
if (!widgetId) {
setError(new Error("hCaptcha not initialized"));
throw new Error("hCaptcha not initialized");
}

setIsLoading(true);
setError(null);
try {
const token = await executeHCaptcha(widgetId);
return token;
} catch (err) {
const error =
err instanceof Error ? err : new Error("Failed to execute captcha");
setError(error);
throw error;
} finally {
setIsLoading(false);
}
}, [widgetId]);

return (
<CaptchaContext.Provider
value={{
executeHCaptcha: handleExecuteHCaptcha,
isReady: !!widgetId,
isLoading,
error,
}}
>
{children}
<Script
src="https://js.hcaptcha.com/1/api.js?render=explicit"
async
defer
onLoad={init}
/>
<div
id="hcaptcha-container"
style={{
display: "none",
position: "fixed",
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: 9999,
overflow: "hidden",
}}
/>
</CaptchaContext.Provider>
);
};
Loading
Loading