From e92d46af369b6c81a30a279e9cb4eb5e309c9c90 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:09:26 +0530 Subject: [PATCH 1/2] refactor(onboarding): remove the legacy /signup free-trial wizard The multi-step Free Trial onboarding wizard served at /signup (and its /user/verify-email redirect) has been fully replaced by the onboarding picker at /onboarding, reached through the RequireOnboarding gate, which auto-starts the trial and routes first deploys through the configure page. Delete the wizard end-to-end: OnboardingPage/Container/View/Stepper, all step components, VerifyEmailPage, and the /signup and /user/verify-email routes. Relocate the still-shared VerificationCodeInput to the auth EmailCodeVerify component that now owns email verification, and drop the now-orphaned UrlService.onboarding() builder and ONBOARDING_STEP_KEY. --- .../EmailCodeVerify/EmailCodeVerify.spec.tsx | 2 +- .../auth/EmailCodeVerify/EmailCodeVerify.tsx | 4 +- .../VerificationCodeInput.spec.tsx | 0 .../VerificationCodeInput.tsx | 0 .../OnboardingContainer.spec.tsx | 453 ----------------- .../OnboardingContainer.tsx | 351 ------------- .../components/onboarding/OnboardingPage.tsx | 17 - .../OnboardingStepper/OnboardingStepper.tsx | 40 -- .../OnboardingView/OnboardingView.tsx | 100 ---- .../VerifyEmailPage/VerifyEmailPage.spec.tsx | 39 -- .../VerifyEmailPage/VerifyEmailPage.tsx | 36 -- .../EmailVerificationContainer.spec.tsx | 116 ----- .../EmailVerificationContainer.tsx | 49 -- .../EmailVerificationStep.spec.tsx | 145 ------ .../EmailVerificationStep.tsx | 113 ----- .../FreeTrialLandingStep.tsx | 122 ----- .../PaymentMethodContainer.spec.tsx | 471 ------------------ .../PaymentMethodContainer.tsx | 267 ---------- .../PaymentMethodStep/PaymentMethodStep.tsx | 174 ------- .../EmptyPaymentMethods.tsx | 15 - .../PaymentMethodsDisplay/ErrorAlert.tsx | 26 - .../PaymentMethodsDisplay.spec.tsx | 154 ------ .../PaymentMethodsDisplay.tsx | 52 -- .../TermsAndConditions.tsx | 19 - .../TrialStartButton.tsx | 16 - .../PaymentVerificationCard.tsx | 75 --- .../steps/WelcomeStep/TemplateCard.tsx | 27 - .../steps/WelcomeStep/TrialStatusBar.tsx | 58 --- .../steps/WelcomeStep/WelcomeStep.tsx | 135 ----- .../PaymentMethodsList/PaymentMethodsList.tsx | 65 --- .../shared/PaymentMethodsList/index.ts | 1 - .../deploy-web/src/hooks/useReturnTo/index.ts | 1 - apps/deploy-web/src/hooks/useTrialBalance.ts | 55 -- apps/deploy-web/src/pages/signup/index.tsx | 10 - .../src/pages/user/verify-email/index.tsx | 4 - .../services/auth/auth/auth.service.spec.ts | 12 +- .../src/services/auth/auth/auth.service.ts | 6 +- apps/deploy-web/src/services/storage/keys.ts | 1 - apps/deploy-web/src/utils/urlUtils.ts | 1 - apps/deploy-web/tests/ui/actions/auth.ts | 4 - .../tests/ui/pages/OnboardingPage.ts | 71 --- 41 files changed, 7 insertions(+), 3300 deletions(-) rename apps/deploy-web/src/components/{onboarding/steps/EmailVerificationStep => auth/EmailCodeVerify}/VerificationCodeInput.spec.tsx (100%) rename apps/deploy-web/src/components/{onboarding/steps/EmailVerificationStep => auth/EmailCodeVerify}/VerificationCodeInput.tsx (100%) delete mode 100644 apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.spec.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/OnboardingPage.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/OnboardingStepper/OnboardingStepper.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/OnboardingView/OnboardingView.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/VerifyEmailPage/VerifyEmailPage.spec.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/VerifyEmailPage/VerifyEmailPage.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/EmailVerificationContainer/EmailVerificationContainer.spec.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/EmailVerificationContainer/EmailVerificationContainer.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/EmailVerificationStep.spec.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/EmailVerificationStep.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/FreeTrialLandingStep/FreeTrialLandingStep.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.spec.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/WelcomeStep/TemplateCard.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/WelcomeStep/TrialStatusBar.tsx delete mode 100644 apps/deploy-web/src/components/onboarding/steps/WelcomeStep/WelcomeStep.tsx delete mode 100644 apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx delete mode 100644 apps/deploy-web/src/components/shared/PaymentMethodsList/index.ts delete mode 100644 apps/deploy-web/src/hooks/useReturnTo/index.ts delete mode 100644 apps/deploy-web/src/hooks/useTrialBalance.ts delete mode 100644 apps/deploy-web/src/pages/signup/index.tsx delete mode 100644 apps/deploy-web/src/pages/user/verify-email/index.tsx delete mode 100644 apps/deploy-web/src/services/storage/keys.ts diff --git a/apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.spec.tsx b/apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.spec.tsx index 17f04ec52c..d2cc75b8cb 100644 --- a/apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.spec.tsx +++ b/apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.spec.tsx @@ -2,10 +2,10 @@ import { forwardRef, useImperativeHandle } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { mock } from "vitest-mock-extended"; -import type { VerificationCodeInputRef } from "@src/components/onboarding/steps/EmailVerificationStep/VerificationCodeInput"; import type { AnalyticsService } from "@src/services/analytics/analytics.service"; import type { AuthService } from "@src/services/auth/auth/auth.service"; import { DEPENDENCIES, EmailCodeVerify, RESEND_COOLDOWN_SEC } from "./EmailCodeVerify"; +import type { VerificationCodeInputRef } from "./VerificationCodeInput"; import { act, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; diff --git a/apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.tsx b/apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.tsx index b15600c042..41ba1d05c9 100644 --- a/apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.tsx +++ b/apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.tsx @@ -4,11 +4,11 @@ import { useEffect, useRef, useState } from "react"; import { Button, Spinner } from "@akashnetwork/ui/components"; import { useMutation } from "@tanstack/react-query"; -import type { VerificationCodeInputRef } from "@src/components/onboarding/steps/EmailVerificationStep/VerificationCodeInput"; -import { VerificationCodeInput } from "@src/components/onboarding/steps/EmailVerificationStep/VerificationCodeInput"; import { RemoteApiError } from "@src/components/shared/RemoteApiError/RemoteApiError"; import { useServices } from "@src/context/ServicesProvider"; import { markCodeSent, readCodeSentAt } from "../PasswordlessAuth/withPersistedPasswordlessFlow"; +import type { VerificationCodeInputRef } from "./VerificationCodeInput"; +import { VerificationCodeInput } from "./VerificationCodeInput"; /** Resend cooldown in seconds, applied on arrival and after each manual resend. */ export const RESEND_COOLDOWN_SEC = 30; diff --git a/apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/VerificationCodeInput.spec.tsx b/apps/deploy-web/src/components/auth/EmailCodeVerify/VerificationCodeInput.spec.tsx similarity index 100% rename from apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/VerificationCodeInput.spec.tsx rename to apps/deploy-web/src/components/auth/EmailCodeVerify/VerificationCodeInput.spec.tsx diff --git a/apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/VerificationCodeInput.tsx b/apps/deploy-web/src/components/auth/EmailCodeVerify/VerificationCodeInput.tsx similarity index 100% rename from apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/VerificationCodeInput.tsx rename to apps/deploy-web/src/components/auth/EmailCodeVerify/VerificationCodeInput.tsx diff --git a/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.spec.tsx b/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.spec.tsx deleted file mode 100644 index c9b54834a3..0000000000 --- a/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.spec.tsx +++ /dev/null @@ -1,453 +0,0 @@ -import React, { useState } from "react"; -import type { ReadonlyURLSearchParams } from "next/navigation"; -import type { Router } from "next/router"; -import { describe, expect, it, type Mock, vi } from "vitest"; -import { mock } from "vitest-mock-extended"; - -import type { AnalyticsService } from "@src/services/analytics/analytics.service"; -import type { AuthService } from "@src/services/auth/auth/auth.service"; -import type { ErrorHandlerService } from "@src/services/error-handler/error-handler.service"; -import { RouteStep } from "@src/types/route-steps.type"; -import type { TransactionMessageData } from "@src/utils/TransactionMessageData"; -import { UrlService } from "@src/utils/urlUtils"; -import { OnboardingContainer, OnboardingStepIndex } from "./OnboardingContainer"; - -import { act, render } from "@testing-library/react"; - -describe("OnboardingContainer", () => { - it("should initialize with default state", () => { - const { child } = setup(); - - expect(child.mock.calls[0][0]).toEqual({ - currentStep: OnboardingStepIndex.FREE_TRIAL, - steps: expect.arrayContaining([ - expect.objectContaining({ id: "free-trial", title: "Free Trial" }), - expect.objectContaining({ id: "signup", title: "Create Account" }), - expect.objectContaining({ id: "email-verification", title: "Verify Email" }), - expect.objectContaining({ id: "payment-method", title: "Payment Method" }), - expect.objectContaining({ id: "welcome", title: "Welcome" }) - ]), - onStepChange: expect.any(Function), - onStepComplete: expect.any(Function), - onStartTrial: expect.any(Function), - onPaymentMethodComplete: expect.any(Function), - onComplete: expect.any(Function) - }); - }); - - it("should track analytics when step changes", async () => { - const { child, mockAnalyticsService } = setup(); - - const { onStepChange } = child.mock.calls[0][0]; - await act(async () => { - onStepChange(OnboardingStepIndex.EMAIL_VERIFICATION); - }); - - expect(mockAnalyticsService.track).toHaveBeenCalledWith("onboarding_step_started", { - category: "onboarding", - step: "email_verification", - step_index: OnboardingStepIndex.EMAIL_VERIFICATION - }); - }); - - it("should track analytics when step completes", async () => { - const { child, mockAnalyticsService } = setup(); - - const { onStepComplete } = child.mock.calls[0][0]; - await act(async () => { - onStepComplete(OnboardingStepIndex.FREE_TRIAL); - }); - - expect(mockAnalyticsService.track).toHaveBeenCalledWith("onboarding_step_completed", { - category: "onboarding", - step: "free_trial", - step_index: OnboardingStepIndex.FREE_TRIAL - }); - }); - - it("should track analytics and redirect when starting trial", async () => { - const { child, mockAnalyticsService, mockRouter, mockUrlService } = setup(); - (mockUrlService.newSignup as Mock).mockReturnValue("/login?tab=signup"); - - const { onStartTrial } = child.mock.calls[0][0]; - await act(async () => { - onStartTrial(); - }); - - expect(mockAnalyticsService.track).toHaveBeenCalledWith("onboarding_free_trial_started", { - category: "onboarding" - }); - expect(mockUrlService.newSignup).toHaveBeenCalledWith({ fromSignup: "true" }); - expect(mockRouter.push).toHaveBeenCalledWith("/login?tab=signup"); - }); - - it("should track analytics when payment method is completed", async () => { - const { child, mockAnalyticsService } = setup({ - paymentMethods: [{ id: "1", type: "card" }] - }); - - const { onPaymentMethodComplete } = child.mock.calls[0][0]; - const initialStep = child.mock.calls[0][0].currentStep; - - await act(async () => { - onPaymentMethodComplete(); - }); - - expect(mockAnalyticsService.track).toHaveBeenCalledWith("onboarding_payment_method_added", { - category: "onboarding" - }); - - const currentStep = child.mock.calls[child.mock.calls.length - 1][0].currentStep; - expect(currentStep).toBe(initialStep); - }); - - it("should redirect to deployment and connect managed wallet when onboarding is completed", async () => { - const { child, mockRouter, mockUrlService, mockCreateWallet } = setup(); - - const { onComplete } = child.mock.calls[0][0]; - await act(async () => { - await onComplete("hello-akash"); - }); - - expect(mockUrlService.newDeployment).toHaveBeenCalledWith({ - step: RouteStep.createLeases, - dseq: "123" - }); - expect(mockRouter.replace).toHaveBeenCalledWith("/deployments/new"); - expect(mockCreateWallet).toHaveBeenCalled(); - }); - - it("should redirect to home when user has managed wallet and no saved step", async () => { - const { mockNavigateBack } = setup({ - wallet: { hasWallet: true, isWalletCreating: false } - }); - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 10)); - }); - - expect(mockNavigateBack).toHaveBeenCalled(); - }); - - it("should not redirect when user has managed wallet but has saved step", async () => { - const { mockRouter } = setup({ - wallet: { hasWallet: true, isWalletCreating: false }, - savedStep: "2" - }); - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)); - }); - - expect(mockRouter.replace).not.toHaveBeenCalled(); - }); - - it("should handle fromSignup URL parameter", async () => { - const { child, mockAnalyticsService } = setup({ - windowLocation: { - search: "?fromSignup=true", - href: "http://localhost/onboarding?fromSignup=true", - origin: "http://localhost" - }, - windowHistory: { - replaceState: vi.fn() - } - }); - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)); - }); - - expect(mockAnalyticsService.track).toHaveBeenCalledWith("onboarding_account_created", { - category: "onboarding" - }); - - // The component should be on the EMAIL_VERIFICATION step after handling fromSignup - expect(child.mock.calls[child.mock.calls.length - 1][0].currentStep).toBe(OnboardingStepIndex.EMAIL_VERIFICATION); - }); - - it("replaces uakt with managed denom when completing onboarding", async () => { - const { child, mockNewDeploymentData } = setup({ - wallet: { hasWallet: true, denom: "ibc/usdc" } - }); - - const { onComplete } = child.mock.calls[0][0]; - await act(async () => { - await onComplete("hello-akash"); - }); - - const sdlArgument = mockNewDeploymentData.mock.calls[0][0]; - expect(sdlArgument).not.toContain("uakt"); - }); - - it("does not replace uakt when managed denom is uakt", async () => { - const { child, mockNewDeploymentData } = setup({ - wallet: { hasWallet: true, denom: "uakt" } - }); - - const { onComplete } = child.mock.calls[0][0]; - await act(async () => { - await onComplete("hello-akash"); - }); - - const sdlArgument = mockNewDeploymentData.mock.calls[0][0]; - expect(sdlArgument).toBe("mock-sdl-content"); - }); - - it("uses act min deposit when managed denom is uact", async () => { - const { child, mockDenomToUdenom } = setup({ - wallet: { hasWallet: true, denom: "uact" } - }); - - const { onComplete } = child.mock.calls[0][0]; - await act(async () => { - await onComplete("hello-akash"); - }); - - expect(mockDenomToUdenom).toHaveBeenCalledWith(0.5); - }); - - it("uses act min deposit when managed denom is ibc", async () => { - const { child, mockDenomToUdenom } = setup({ - wallet: { hasWallet: true, denom: "ibc/usdc" } - }); - - const { onComplete } = child.mock.calls[0][0]; - await act(async () => { - await onComplete("hello-akash"); - }); - - expect(mockDenomToUdenom).toHaveBeenCalledWith(0.5); - }); - - it("does not replace uakt for self-custody wallet", async () => { - const { child, mockNewDeploymentData } = setup({ - wallet: { hasWallet: false } - }); - - const { onComplete } = child.mock.calls[0][0]; - await act(async () => { - await onComplete("hello-akash"); - }); - - const sdlArgument = mockNewDeploymentData.mock.calls[0][0]; - expect(sdlArgument).toBe("mock-sdl-content"); - expect(sdlArgument).not.toContain("undefined"); - }); - - function setup( - input: { - paymentMethods?: Array<{ id: string; type: string }>; - user?: { emailVerified?: boolean; userId?: string }; - wallet?: { hasWallet?: boolean; isWalletCreating?: boolean; denom?: string }; - windowLocation?: Partial; - windowHistory?: Partial; - savedStep?: string; - } = {} - ) { - vi.clearAllMocks(); - - // Mock localStorage using jest-mock-extended - const mockLocalStorage = mock({ - getItem: vi.fn().mockReturnValue(input.savedStep || null) - }); - - // Store original window objects - let windowLocation = window.location; - let windowHistory = window.history; - - // Mock window.location and history based on input - if (input.windowLocation) { - windowLocation = { ...windowLocation, ...input.windowLocation }; - } else if (!window.location.search) { - // Set default values if window.location is not already mocked - windowLocation = { - ...windowLocation, - href: "http://localhost/onboarding", - origin: "http://localhost", - search: "" - }; - } - - if (input.windowHistory) { - windowHistory = { ...windowHistory, ...input.windowHistory }; - } - - const mockAnalyticsService = mock(); - const mockRouter = mock(); - const authService = mock(); - const mockCreateWallet = vi.fn(); - const mockSignAndBroadcastTx = vi.fn().mockResolvedValue({ transactionHash: "mock-hash" }); - - const mockUrlService = { - ...UrlService, - onboarding: vi.fn(() => "/onboarding"), - signup: vi.fn(() => "/signup"), - newSignup: vi.fn(() => "/login?tab=signup"), - newDeployment: vi.fn(() => "/deployments/new") - }; - - const mockDeploymentLocalStorage = { - update: vi.fn() - }; - - const mockAppConfig = { - NEXT_PUBLIC_DEFAULT_INITIAL_DEPOSIT: "5000000", - NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID: "sandbox" - }; - - const mockUseUser = vi.fn().mockReturnValue(input.user || { emailVerified: false }); - const mockUsePaymentMethodsQuery = vi.fn().mockReturnValue({ data: input.paymentMethods || [] }); - const mockUseChainParam = vi.fn().mockReturnValue({ minDeposit: { akt: 0.5, usdc: 5, act: 0.5 } }); - const mockDenomToUdenom = vi.fn().mockImplementation((amount: number) => amount * 1_000_000); - const mockErrorHandler = mock(); - const mockTemplateService = { - findById: vi.fn().mockResolvedValue({ deploy: "mock-template-sdl" }) - }; - - const mockUseServices = vi.fn().mockReturnValue({ - analyticsService: mockAnalyticsService, - urlService: mockUrlService, - authService, - deploymentLocalStorage: mockDeploymentLocalStorage, - publicConfig: mockAppConfig, - errorHandler: mockErrorHandler, - windowLocation, - windowHistory, - template: mockTemplateService - }); - const mockUseRouter = vi.fn().mockReturnValue(mockRouter); - const mockUseWallet = vi.fn().mockReturnValue({ - hasWallet: input.wallet?.hasWallet || false, - isWalletCreating: input.wallet?.isWalletCreating || false, - denom: input.wallet?.denom, - createWallet: mockCreateWallet, - address: "akash1test", - signAndBroadcastTx: mockSignAndBroadcastTx - }); - const mockUseSnackbar = vi.fn().mockReturnValue({ - enqueueSnackbar: vi.fn() - }); - const mockNotificator = { success: vi.fn(), error: vi.fn() }; - const mockUseNotificator = vi.fn().mockReturnValue(mockNotificator); - const mockNavigateBack = vi.fn(); - const mockNavigateWithReturnTo = vi.fn(); - const mockUseReturnTo = vi.fn().mockReturnValue({ - returnTo: "/", - navigateWithReturnTo: mockNavigateWithReturnTo, - navigateBack: mockNavigateBack, - hasReturnTo: true, - isDeploymentReturnTo: false - }); - - const params = new URLSearchParams(); - if (input.windowLocation?.search) { - const searchParams = new URLSearchParams(input.windowLocation.search.replace("?", "")); - searchParams.forEach((value, key) => { - params.set(key, value); - }); - } - const useSearchParams = () => { - const [pageParams] = useState(params); - return pageParams as ReadonlyURLSearchParams; - }; - - const mockNewDeploymentData = vi.fn().mockResolvedValue({ - deploymentId: { dseq: "123" }, - hash: "mock-hash" - }); - - const mockDeploymentData = { - NewDeploymentData: mockNewDeploymentData, - getManifest: vi.fn(), - getManifestVersion: vi.fn(), - appendTrialAttribute: vi.fn(), - appendAuditorRequirement: vi.fn(sdl => sdl), - applyTrialGpuPolicy: vi.fn((sdl: string) => sdl), - isTrialBlockedGpuModel: vi.fn(() => false), - isTrialBlockedGpuSelection: vi.fn(() => false), - isTrialGpuRestrictionActive: vi.fn(() => false), - hasTrialBlockedGpu: vi.fn(() => false), - replaceSdlDenom: vi.fn((sdl: string) => sdl), - ENDPOINT_NAME_VALIDATION_REGEX: /^[a-z]+[-_\da-z]+$/, - TRIAL_ATTRIBUTE: "console/trials" as const, - TRIAL_REGISTERED_ATTRIBUTE: "console/trials-registered" as const, - AUDITOR: "akash1365yvmc4s7awdyj3n2sav7xfx76adc6dnmlx63" as const, - MANAGED_WALLET_ALLOWED_AUDITORS: ["akash1365yvmc4s7awdyj3n2sav7xfx76adc6dnmlx63" as const] - }; - - const mockValidateDeploymentData = vi.fn(); - const mockAppendAuditorRequirement = vi.fn(sdl => sdl); - const mockHelloWorldTemplate = { - title: "Hello World", - name: "Hello World", - code: "hello-world", - category: "General", - description: "Simple next.js web application showing hello world.", - githubUrl: "https://github.com/akash-network/hello-akash-world", - valuesToChange: [], - content: "mock-sdl-content" - }; - const mockTransactionMessageData = { - prototype: {}, - getCreateLeaseMsg: vi.fn(), - getCreateDeploymentMsg: vi.fn(), - getUpdateDeploymentMsg: vi.fn(), - getDepositDeploymentMsg: vi.fn(), - getCloseDeploymentMsg: vi.fn(), - getSendTokensMsg: vi.fn() - }; - - // Create dependencies object - const dependencies = { - useUser: mockUseUser, - usePaymentMethodsQuery: mockUsePaymentMethodsQuery, - useChainParam: mockUseChainParam, - useServices: mockUseServices, - useRouter: mockUseRouter, - useWallet: mockUseWallet, - useSnackbar: mockUseSnackbar, - useNotificator: mockUseNotificator, - useReturnTo: mockUseReturnTo, - localStorage: mockLocalStorage, - deploymentData: mockDeploymentData, - validateDeploymentData: mockValidateDeploymentData, - appendAuditorRequirement: mockAppendAuditorRequirement, - applyTrialGpuPolicy: vi.fn((sdl: string) => sdl), - replaceSdlDenom: vi.fn((sdl: string, denom: string) => sdl.replace(/uakt/g, denom)), - helloWorldTemplate: mockHelloWorldTemplate, - TransactionMessageData: mockTransactionMessageData as unknown as typeof TransactionMessageData, - useSearchParams, - denomToUdenom: mockDenomToUdenom - }; - - const mockChildren = vi.fn().mockReturnValue(
Test
); - - render({mockChildren}); - - return { - child: mockChildren, - mockAnalyticsService, - mockRouter, - mockUrlService, - authService, - mockUseUser, - mockUsePaymentMethodsQuery, - mockUseChainParam, - mockDenomToUdenom, - mockUseServices, - mockUseRouter, - mockCreateWallet, - mockNavigateBack, - mockNavigateWithReturnTo, - mockLocalStorage, - mockSignAndBroadcastTx, - mockDeploymentLocalStorage, - mockNewDeploymentData, - mockValidateDeploymentData, - mockAppendAuditorRequirement, - mockTransactionMessageData, - mockUseWallet - }; - } -}); diff --git a/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx b/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx deleted file mode 100644 index fcda88caef..0000000000 --- a/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx +++ /dev/null @@ -1,351 +0,0 @@ -"use client"; -import type { ReactNode } from "react"; -import React, { useCallback, useEffect, useState } from "react"; -import type { EncodeObject } from "@cosmjs/proto-signing"; -import { useRouter, useSearchParams } from "next/navigation"; - -import { SuccessAnimation } from "@src/components/shared"; -import { useServices } from "@src/context/ServicesProvider"; -import { useWallet } from "@src/context/WalletProvider"; -import { useChainParam } from "@src/hooks/useChainParam/useChainParam"; -import { useNotificator } from "@src/hooks/useNotificator"; -import { useReturnTo } from "@src/hooks/useReturnTo"; -import { useUser } from "@src/hooks/useUser"; -import { usePaymentMethodsQuery } from "@src/queries/usePaymentQueries"; -import { ONBOARDING_STEP_KEY } from "@src/services/storage/keys"; -import { RouteStep } from "@src/types/route-steps.type"; -import { deploymentData } from "@src/utils/deploymentData"; -import { appendAuditorRequirement, applyTrialGpuPolicy, replaceSdlDenom } from "@src/utils/deploymentData/v1beta3"; -import { validateDeploymentData } from "@src/utils/deploymentUtils"; -import { denomToUdenom } from "@src/utils/mathHelpers"; -import { helloWorldTemplate } from "@src/utils/templates"; -import { TransactionMessageData } from "@src/utils/TransactionMessageData"; -import { type OnboardingStep } from "../OnboardingStepper/OnboardingStepper"; - -export enum OnboardingStepIndex { - FREE_TRIAL = 0, - SIGNUP = 1, - EMAIL_VERIFICATION = 2, - PAYMENT_METHOD = 3, - WELCOME = 4 -} - -export type OnboardingContainerProps = { - children: (props: { - currentStep: number; - steps: OnboardingStep[]; - onStepChange: (step: number) => void; - onStepComplete: (step: OnboardingStepIndex) => void; - onStartTrial: () => void; - onPaymentMethodComplete: () => void; - onComplete: (templateName?: string) => Promise; - }) => ReactNode; - dependencies?: typeof DEPENDENCIES; -}; - -const DEPENDENCIES = { - useUser, - usePaymentMethodsQuery, - useChainParam, - useServices, - useRouter, - useWallet, - useNotificator, - useReturnTo, - // eslint-disable-next-line akash/dependencies-component-or-hook - localStorage: typeof window !== "undefined" ? window.localStorage : null, - // eslint-disable-next-line akash/dependencies-component-or-hook - deploymentData, - // eslint-disable-next-line akash/dependencies-component-or-hook - validateDeploymentData, - // eslint-disable-next-line akash/dependencies-component-or-hook - appendAuditorRequirement, - // eslint-disable-next-line akash/dependencies-component-or-hook - applyTrialGpuPolicy, - // eslint-disable-next-line akash/dependencies-component-or-hook - replaceSdlDenom, - // eslint-disable-next-line akash/dependencies-component-or-hook - helloWorldTemplate, - TransactionMessageData, - useSearchParams, - // eslint-disable-next-line akash/dependencies-component-or-hook - denomToUdenom -}; - -export const OnboardingContainer: React.FunctionComponent = ({ children, dependencies: d = DEPENDENCIES }) => { - const [currentStep, setCurrentStep] = useState(OnboardingStepIndex.FREE_TRIAL); - const [completedSteps, setCompletedSteps] = useState>(new Set()); - const [showSuccessAnimation, setShowSuccessAnimation] = useState(false); - - const router = d.useRouter(); - const searchParams = d.useSearchParams(); - const { user } = d.useUser(); - const { data: paymentMethods = [] } = d.usePaymentMethodsQuery({ enabled: !!user?.stripeCustomerId }); - const { minDeposit } = d.useChainParam(); - const { analyticsService, urlService, deploymentLocalStorage, errorHandler, windowLocation, windowHistory, template: templateService } = d.useServices(); - const wallet = d.useWallet(); - const notificator = d.useNotificator(); - const { navigateBack } = d.useReturnTo({ defaultReturnTo: "/" }); - - useEffect(() => { - const savedStep = d.localStorage?.getItem(ONBOARDING_STEP_KEY); - if (!wallet.isWalletCreating && wallet.hasWallet && !savedStep) { - navigateBack(); - } - }, [wallet.isWalletCreating, wallet.hasWallet, d.localStorage, navigateBack]); - - useEffect(() => { - const savedStep = d.localStorage?.getItem(ONBOARDING_STEP_KEY); - if (savedStep) { - const step = parseInt(savedStep, 10); - if (step >= 0 && step < Object.keys(OnboardingStepIndex).length / 2) { - setCurrentStep(step); - } - } - - const fromSignup = searchParams.get("fromSignup"); - if (fromSignup === "true") { - analyticsService.track("onboarding_account_created", { - category: "onboarding" - }); - - setCompletedSteps(prev => new Set([...prev, OnboardingStepIndex.SIGNUP])); - setCurrentStep(OnboardingStepIndex.EMAIL_VERIFICATION); - d.localStorage?.setItem(ONBOARDING_STEP_KEY, OnboardingStepIndex.EMAIL_VERIFICATION.toString()); - - const newUrl = new URL(windowLocation.href); - newUrl.searchParams.delete("fromSignup"); - windowHistory.replaceState({}, "", newUrl.toString()); - } - }, [analyticsService, d.localStorage, searchParams, windowLocation, windowHistory]); - - const handleStepChange = useCallback( - (step: number) => { - if (step === OnboardingStepIndex.PAYMENT_METHOD && currentStep === OnboardingStepIndex.EMAIL_VERIFICATION) { - if (!user?.emailVerified) { - return; - } - } - - if (step === OnboardingStepIndex.WELCOME && currentStep === OnboardingStepIndex.PAYMENT_METHOD) { - if (paymentMethods.length === 0) { - return; - } - } - - const stepNames = ["free_trial", "signup", "email_verification", "payment_method", "welcome"]; - analyticsService.track("onboarding_step_started", { - category: "onboarding", - step: stepNames[step], - step_index: step - }); - - setCurrentStep(step); - d.localStorage?.setItem(ONBOARDING_STEP_KEY, step.toString()); - }, - [currentStep, user?.emailVerified, paymentMethods.length, analyticsService, d.localStorage] - ); - - const handleStepComplete = useCallback( - (step: OnboardingStepIndex) => { - const stepNames = ["free_trial", "signup", "email_verification", "payment_method", "welcome"]; - analyticsService.track("onboarding_step_completed", { - category: "onboarding", - step: stepNames[step], - step_index: step - }); - - setCompletedSteps(prev => new Set([...prev, step])); - }, - [analyticsService] - ); - - const handleStartTrial = useCallback(() => { - analyticsService.track("onboarding_free_trial_started", { - category: "onboarding" - }); - - handleStepComplete(OnboardingStepIndex.FREE_TRIAL); - - if (user?.userId) { - if (user.emailVerified) { - setCompletedSteps(prev => new Set([...prev, OnboardingStepIndex.SIGNUP, OnboardingStepIndex.EMAIL_VERIFICATION])); - handleStepChange(OnboardingStepIndex.PAYMENT_METHOD); - } else { - setCompletedSteps(prev => new Set([...prev, OnboardingStepIndex.SIGNUP])); - handleStepChange(OnboardingStepIndex.EMAIL_VERIFICATION); - } - } else { - router.push(urlService.newSignup({ fromSignup: "true" })); - } - }, [analyticsService, handleStepComplete, user?.userId, user?.emailVerified, handleStepChange, router, urlService]); - - const handlePaymentMethodComplete = useCallback(() => { - if (paymentMethods.length > 0) { - analyticsService.track("onboarding_payment_method_added", { - category: "onboarding" - }); - - handleStepComplete(OnboardingStepIndex.PAYMENT_METHOD); - setShowSuccessAnimation(true); - } - }, [paymentMethods.length, analyticsService, handleStepComplete]); - - const handleSuccessAnimationComplete = useCallback(() => { - setShowSuccessAnimation(false); - handleStepChange(OnboardingStepIndex.WELCOME); - }, [handleStepChange]); - - const complete = useCallback( - async (templateName?: string) => { - if (!templateName) { - d.localStorage?.removeItem(ONBOARDING_STEP_KEY); - navigateBack(); - return; - } - - try { - const templateMap: Record = { - "hello-akash": { - sdl: d.helloWorldTemplate.content, - name: "Hello Akash" - }, - comfyui: { - id: "akash-network-awesome-akash-comfyui", - sdl: "", - name: "ComfyUI" - }, - "llama-3.1-8b": { - id: "akash-network-awesome-akash-Llama-3.1-8B", - sdl: "", - name: "Llama 3.1 8B" - } - }; - - const templateConfig = templateMap[templateName]; - if (!templateConfig) { - const error = new Error(`Template ${templateName} not found`); - errorHandler.reportError({ - error, - severity: "warning", - tags: { component: "onboarding", template: templateName } - }); - notificator.error(`Template "${templateName}" is no longer supported, please choose another one`); - return; - } - - let sdl = templateConfig.sdl; - - if (templateConfig.id) { - const template = await templateService.findById(templateConfig.id); - if (!template || !template.deploy) { - const error = new Error(`Template ${templateName} SDL not found`); - errorHandler.reportError({ - error, - severity: "warning", - tags: { component: "onboarding", template: templateName, templateId: templateConfig.id } - }); - notificator.error(`Template "${templateConfig.name}" is no longer supported, please choose another one`); - return; - } - sdl = template.deploy; - } - - sdl = d.appendAuditorRequirement(sdl); - sdl = d.replaceSdlDenom(sdl, wallet.denom); - if (wallet.isTrialing) { - sdl = d.applyTrialGpuPolicy(sdl); - } - - const deposit = d.denomToUdenom(minDeposit.act); - const dd = await d.deploymentData.NewDeploymentData(sdl, null, wallet.address, deposit); - d.validateDeploymentData(dd, null); - - if (!dd) { - throw new Error("Failed to create deployment data"); - } - - const messages: EncodeObject[] = []; - messages.push(d.TransactionMessageData.getCreateDeploymentMsg(dd)); - const response = await wallet.signAndBroadcastTx(messages); - - if (response) { - deploymentLocalStorage.update(wallet.address, dd.deploymentId.dseq, { - manifest: sdl, - manifestVersion: dd.hash, - name: templateConfig.name - }); - - analyticsService.track("create_deployment", { - category: "onboarding", - template: templateName, - dseq: dd.deploymentId.dseq - }); - - d.localStorage?.removeItem(ONBOARDING_STEP_KEY); - wallet.createWallet(); - router.replace(urlService.newDeployment({ step: RouteStep.createLeases, dseq: dd.deploymentId.dseq })); - } - } catch { - notificator.error("Failed to deploy template. Please try again."); - } - }, - [d, router, urlService, wallet, templateService, minDeposit, deploymentLocalStorage, analyticsService, notificator, errorHandler, navigateBack] - ); - - const steps: OnboardingStep[] = [ - { - id: "free-trial", - title: "Free Trial", - description: "Learn about benefits", - component: null, - isCompleted: completedSteps.has(OnboardingStepIndex.FREE_TRIAL) - }, - { - id: "signup", - title: "Create Account", - description: "Sign up with Auth0", - component: null, - isCompleted: completedSteps.has(OnboardingStepIndex.SIGNUP) - }, - { - id: "email-verification", - title: "Verify Email", - description: "Confirm your email", - component: null, - isCompleted: completedSteps.has(OnboardingStepIndex.EMAIL_VERIFICATION), - isDisabled: !user?.emailVerified, - hidePreviousButton: true - }, - { - id: "payment-method", - title: "Payment Method", - description: "Add payment info", - component: null, - isCompleted: completedSteps.has(OnboardingStepIndex.PAYMENT_METHOD), - isDisabled: paymentMethods.length === 0 - }, - { - id: "welcome", - title: "Welcome", - description: "Get started", - component: null, - isCompleted: completedSteps.has(OnboardingStepIndex.WELCOME) - } - ]; - - return ( - <> - {children({ - currentStep, - steps, - onStepChange: handleStepChange, - onStepComplete: handleStepComplete, - onStartTrial: handleStartTrial, - onPaymentMethodComplete: handlePaymentMethodComplete, - onComplete: complete - })} - - - ); -}; diff --git a/apps/deploy-web/src/components/onboarding/OnboardingPage.tsx b/apps/deploy-web/src/components/onboarding/OnboardingPage.tsx deleted file mode 100644 index 250aa55f5d..0000000000 --- a/apps/deploy-web/src/components/onboarding/OnboardingPage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { type FC } from "react"; - -import { CustomNextSeo } from "@src/components/shared/CustomNextSeo"; -import { domainName, UrlService } from "@src/utils/urlUtils"; -import { OnboardingContainer } from "./OnboardingContainer/OnboardingContainer"; -import { OnboardingView } from "./OnboardingView/OnboardingView"; - -export const OnboardingPage: FC = () => { - return ( -
- -
- {props => } -
-
- ); -}; diff --git a/apps/deploy-web/src/components/onboarding/OnboardingStepper/OnboardingStepper.tsx b/apps/deploy-web/src/components/onboarding/OnboardingStepper/OnboardingStepper.tsx deleted file mode 100644 index c2d1ed47ae..0000000000 --- a/apps/deploy-web/src/components/onboarding/OnboardingStepper/OnboardingStepper.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; -import React from "react"; - -import { type Step, Stepper } from "@src/components/shared/Stepper"; - -export interface OnboardingStep { - id: string; - title: string; - description?: string; - component: React.ReactNode | null; - isCompleted?: boolean; - isDisabled?: boolean; - hidePreviousButton?: boolean; -} - -interface OnboardingStepperProps { - steps: OnboardingStep[]; - currentStep: number; - className?: string; -} - -export const OnboardingStepper: React.FunctionComponent = ({ steps, currentStep, className = "" }) => { - const currentStepData = steps[currentStep]; - - const stepperSteps: Step[] = steps.map((step, _index) => ({ - id: step.id, - name: step.title, - description: step.description - })); - - return ( -
-
- -
- - {currentStepData?.component &&
{currentStepData.component}
} -
- ); -}; diff --git a/apps/deploy-web/src/components/onboarding/OnboardingView/OnboardingView.tsx b/apps/deploy-web/src/components/onboarding/OnboardingView/OnboardingView.tsx deleted file mode 100644 index b64213cd3c..0000000000 --- a/apps/deploy-web/src/components/onboarding/OnboardingView/OnboardingView.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import type { FC } from "react"; -import React from "react"; -import { buttonVariants } from "@akashnetwork/ui/components"; -import { cn } from "@akashnetwork/ui/utils"; -import { LogOut } from "iconoir-react"; - -import { useServices } from "@src/context/ServicesProvider"; -import { useUser } from "@src/hooks/useUser"; -import { OnboardingStepIndex } from "../OnboardingContainer/OnboardingContainer"; -import { type OnboardingStep, OnboardingStepper } from "../OnboardingStepper/OnboardingStepper"; -import { EmailVerificationContainer } from "../steps/EmailVerificationContainer/EmailVerificationContainer"; -import { EmailVerificationStep } from "../steps/EmailVerificationStep/EmailVerificationStep"; -import { FreeTrialLandingStep } from "../steps/FreeTrialLandingStep/FreeTrialLandingStep"; -import { PaymentMethodContainer } from "../steps/PaymentMethodContainer/PaymentMethodContainer"; -import { PaymentMethodStep } from "../steps/PaymentMethodStep/PaymentMethodStep"; -import { WelcomeStep } from "../steps/WelcomeStep/WelcomeStep"; - -const DEPENDENCIES = { - OnboardingStepper, - FreeTrialLandingStep, - EmailVerificationContainer, - EmailVerificationStep, - PaymentMethodContainer, - PaymentMethodStep, - WelcomeStep -}; - -export interface OnboardingViewProps { - currentStep: number; - steps: OnboardingStep[]; - onStepChange: (step: number) => void; - onStartTrial: () => void; - onPaymentMethodComplete: () => void; - onComplete: (templateName?: string) => Promise; - dependencies?: typeof DEPENDENCIES; -} - -export const OnboardingView: FC = ({ - currentStep, - steps, - onStepChange, - onStartTrial, - onPaymentMethodComplete, - onComplete, - dependencies: d = DEPENDENCIES -}) => { - const stepsWithComponents: OnboardingStep[] = [ - { - ...steps[OnboardingStepIndex.FREE_TRIAL], - component: - }, - { - ...steps[OnboardingStepIndex.SIGNUP], - component: null // No component needed for redirect step - }, - { - ...steps[OnboardingStepIndex.EMAIL_VERIFICATION], - component: ( - onStepChange(OnboardingStepIndex.PAYMENT_METHOD)}> - {({ sendCode, verifyCode }) => } - - ) - }, - { - ...steps[OnboardingStepIndex.PAYMENT_METHOD], - component: {props => } - }, - { - ...steps[OnboardingStepIndex.WELCOME], - component: - } - ]; - - const { analyticsService, authService } = useServices(); - const { user } = useUser(); - - const handleLogout = () => { - analyticsService.track("onboarding_logout", { - category: "onboarding" - }); - authService.logout(); - }; - - return ( - <> - - {user && currentStep !== OnboardingStepIndex.WELCOME && ( -
- -
- )} - - ); -}; diff --git a/apps/deploy-web/src/components/onboarding/VerifyEmailPage/VerifyEmailPage.spec.tsx b/apps/deploy-web/src/components/onboarding/VerifyEmailPage/VerifyEmailPage.spec.tsx deleted file mode 100644 index 68a8b65536..0000000000 --- a/apps/deploy-web/src/components/onboarding/VerifyEmailPage/VerifyEmailPage.spec.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { ComponentProps } from "react"; -import React from "react"; -import { describe, expect, it, vi } from "vitest"; - -import { VerifyEmailPage } from "./VerifyEmailPage"; - -import { render, screen } from "@testing-library/react"; - -describe(VerifyEmailPage.name, () => { - it("shows redirect loading text", () => { - setup(); - - expect(screen.queryByText("Redirecting to email verification...")).toBeInTheDocument(); - }); - - it("redirects to onboarding with email verification step", () => { - const { redirect } = setup(); - - expect(redirect).toHaveBeenCalledWith("/signup?return-to=%2F"); - }); - - function setup(input: { onboardingUrl?: string } = {}) { - const redirect = vi.fn(); - - const dependencies = { - Layout: ({ children }: { children: React.ReactNode }) =>
{children}
, - Loading: ({ text }: { text: string }) =>
{text}
, - NextSeo: () => null, - UrlService: { - onboarding: vi.fn(() => input.onboardingUrl ?? "/signup?return-to=%2F") - }, - redirect - } as unknown as ComponentProps["dependencies"]; - - render(); - - return { redirect }; - } -}); diff --git a/apps/deploy-web/src/components/onboarding/VerifyEmailPage/VerifyEmailPage.tsx b/apps/deploy-web/src/components/onboarding/VerifyEmailPage/VerifyEmailPage.tsx deleted file mode 100644 index 63357d9bf8..0000000000 --- a/apps/deploy-web/src/components/onboarding/VerifyEmailPage/VerifyEmailPage.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { useEffect } from "react"; -import { NextSeo } from "next-seo"; - -import Layout, { Loading } from "@src/components/layout/Layout"; -import { OnboardingStepIndex } from "@src/components/onboarding/OnboardingContainer/OnboardingContainer"; -import { ONBOARDING_STEP_KEY } from "@src/services/storage/keys"; -import { UrlService } from "@src/utils/urlUtils"; - -const DEPENDENCIES = { - Layout, - Loading, - NextSeo, - UrlService, - // eslint-disable-next-line akash/dependencies-component-or-hook - redirect: (url: string) => { - window.localStorage.setItem(ONBOARDING_STEP_KEY, OnboardingStepIndex.EMAIL_VERIFICATION.toString()); - window.location.replace(url); - } -}; - -type VerifyEmailPageProps = { - dependencies?: typeof DEPENDENCIES; -}; - -export function VerifyEmailPage({ dependencies: d = DEPENDENCIES }: VerifyEmailPageProps) { - useEffect(() => { - d.redirect(d.UrlService.onboarding({ returnTo: "/" })); - }, [d]); - - return ( - - - - - ); -} diff --git a/apps/deploy-web/src/components/onboarding/steps/EmailVerificationContainer/EmailVerificationContainer.spec.tsx b/apps/deploy-web/src/components/onboarding/steps/EmailVerificationContainer/EmailVerificationContainer.spec.tsx deleted file mode 100644 index 17b2a31016..0000000000 --- a/apps/deploy-web/src/components/onboarding/steps/EmailVerificationContainer/EmailVerificationContainer.spec.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from "react"; -import { describe, expect, it, type Mock, vi } from "vitest"; - -import { EmailVerificationContainer } from "./EmailVerificationContainer"; - -import { act, render } from "@testing-library/react"; - -describe(EmailVerificationContainer.name, () => { - it("renders children with sendCode and verifyCode functions", () => { - const { child } = setup(); - - expect(child).toHaveBeenCalledWith( - expect.objectContaining({ - sendCode: expect.any(Function), - verifyCode: expect.any(Function) - }) - ); - }); - - it("auto-advances when email is already verified", () => { - const mockOnComplete = vi.fn(); - setup({ user: { id: "test-user", emailVerified: true }, onComplete: mockOnComplete }); - - expect(mockOnComplete).toHaveBeenCalled(); - }); - - it("sendCode delegates to auth.sendVerificationCode", async () => { - const { child, mockSendVerificationCode } = setup(); - - mockSendVerificationCode.mockResolvedValue({}); - - const { sendCode } = child.mock.calls[child.mock.calls.length - 1][0]; - await act(async () => { - await sendCode(); - }); - - expect(mockSendVerificationCode).toHaveBeenCalled(); - }); - - it("verifyCode delegates to auth.verifyEmailCode then calls checkSession", async () => { - const { child, mockVerifyEmailCode, mockCheckSession } = setup(); - mockVerifyEmailCode.mockResolvedValue(undefined); - mockCheckSession.mockResolvedValue(undefined); - - const { verifyCode } = child.mock.calls[0][0]; - await act(async () => { - await verifyCode("123456"); - }); - - expect(mockVerifyEmailCode).toHaveBeenCalledWith("123456"); - expect(mockCheckSession).toHaveBeenCalled(); - }); - - it("verifyCode propagates errors", async () => { - const { child, mockVerifyEmailCode } = setup(); - mockVerifyEmailCode.mockRejectedValue(new Error("Invalid verification code")); - - const { verifyCode } = child.mock.calls[0][0]; - await expect( - act(async () => { - await verifyCode("000000"); - }) - ).rejects.toThrow("Invalid verification code"); - }); - - it("tracks analytics on auto-advance", () => { - const mockOnComplete = vi.fn(); - const { mockAnalyticsService } = setup({ user: { id: "test-user", emailVerified: true }, onComplete: mockOnComplete }); - - expect(mockAnalyticsService.track).toHaveBeenCalledWith("onboarding_email_verified", { category: "onboarding" }); - }); - - function setup(input: { user?: { id: string; emailVerified: boolean }; onComplete?: Mock } = {}) { - const mockSendVerificationCode = vi.fn().mockResolvedValue({}); - const mockVerifyEmailCode = vi.fn(); - const mockCheckSession = vi.fn(); - const mockAnalyticsService = { - track: vi.fn() - }; - - const mockUseCustomUser = vi.fn().mockReturnValue({ - user: input.user || { id: "test-user", emailVerified: false }, - checkSession: mockCheckSession - }); - - const mockUseServices = vi.fn().mockReturnValue({ - analyticsService: mockAnalyticsService, - auth: { - sendVerificationCode: mockSendVerificationCode, - verifyEmailCode: mockVerifyEmailCode - } - }); - - const dependencies = { - useCustomUser: mockUseCustomUser, - useServices: mockUseServices - }; - - const mockChildren = vi.fn().mockReturnValue(
Test
); - const mockOnComplete = input.onComplete || vi.fn(); - - render( - - {mockChildren} - - ); - - return { - child: mockChildren, - mockSendVerificationCode, - mockVerifyEmailCode, - mockCheckSession, - mockAnalyticsService - }; - } -}); diff --git a/apps/deploy-web/src/components/onboarding/steps/EmailVerificationContainer/EmailVerificationContainer.tsx b/apps/deploy-web/src/components/onboarding/steps/EmailVerificationContainer/EmailVerificationContainer.tsx deleted file mode 100644 index f60984814a..0000000000 --- a/apps/deploy-web/src/components/onboarding/steps/EmailVerificationContainer/EmailVerificationContainer.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; -import type { FC, ReactNode } from "react"; -import React, { useCallback, useEffect } from "react"; - -import { useServices } from "@src/context/ServicesProvider"; -import { useCustomUser } from "@src/hooks/useCustomUser"; - -export const DEPENDENCIES = { - useCustomUser, - useServices -}; - -export type EmailVerificationContainerProps = { - children: (props: { sendCode: () => Promise; verifyCode: (code: string) => Promise }) => ReactNode; - onComplete: () => void; - dependencies?: typeof DEPENDENCIES; -}; - -export const EmailVerificationContainer: FC = ({ children, onComplete, dependencies: d = DEPENDENCIES }) => { - const { user, checkSession } = d.useCustomUser(); - const { analyticsService, auth } = d.useServices(); - - const advance = useCallback(() => { - analyticsService.track("onboarding_email_verified", { - category: "onboarding" - }); - onComplete(); - }, [analyticsService, onComplete]); - - useEffect(() => { - if (user?.emailVerified) { - advance(); - } - }, [user?.emailVerified, advance]); - - const sendCode = useCallback(async () => { - await auth.sendVerificationCode(); - }, [auth]); - - const verifyCode = useCallback( - async (code: string) => { - await auth.verifyEmailCode(code); - await checkSession(); - }, - [auth, checkSession] - ); - - return <>{children({ sendCode, verifyCode })}; -}; diff --git a/apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/EmailVerificationStep.spec.tsx b/apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/EmailVerificationStep.spec.tsx deleted file mode 100644 index cea746d481..0000000000 --- a/apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/EmailVerificationStep.spec.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import React from "react"; -import { describe, expect, it, vi } from "vitest"; - -import { EmailVerificationStep } from "./EmailVerificationStep"; - -import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; - -describe(EmailVerificationStep.name, () => { - it("renders 6 digit inputs", () => { - setup(); - - const inputs = screen.queryAllByRole("textbox"); - expect(inputs).toHaveLength(6); - }); - - it("renders verification title", () => { - setup(); - - expect(screen.queryByText("Email Verification")).toBeInTheDocument(); - }); - - it("renders code prompt text", () => { - setup(); - - expect(screen.queryByText("We've sent a 6-digit verification code to your email address.")).toBeInTheDocument(); - }); - - it("renders resend button as 'Resend Code' when idle", () => { - setup(); - - expect(screen.queryByText("Resend Code")).toBeInTheDocument(); - }); - - it("calls sendCode when resend button is clicked", async () => { - const { sendCode } = setup(); - - const buttons = screen.queryAllByRole("button"); - const resendButton = buttons.find(b => b.textContent?.includes("Resend Code"))!; - await act(async () => { - fireEvent.click(resendButton); - }); - - expect(sendCode).toHaveBeenCalled(); - }); - - it("shows success notification after resend", async () => { - const { mockNotificator } = setup(); - - const buttons = screen.queryAllByRole("button"); - const resendButton = buttons.find(b => b.textContent?.includes("Resend Code"))!; - await act(async () => { - fireEvent.click(resendButton); - }); - - expect(mockNotificator.success).toHaveBeenCalledWith("Verification code sent. Please check your email for the 6-digit code."); - }); - - it("shows error notification when resend fails", async () => { - const sendCode = vi.fn().mockRejectedValue(new Error("Please wait before requesting a new verification code.")); - const { mockNotificator } = setup({ sendCode }); - - const buttons = screen.queryAllByRole("button"); - const resendButton = buttons.find(b => b.textContent?.includes("Resend Code"))!; - await act(async () => { - fireEvent.click(resendButton); - }); - - expect(mockNotificator.error).toHaveBeenCalledWith("Please wait before requesting a new verification code."); - }); - - it("starts cooldown after successful resend", async () => { - setup(); - - const buttons = screen.queryAllByRole("button"); - const resendButton = buttons.find(b => b.textContent?.includes("Resend Code"))!; - await act(async () => { - fireEvent.click(resendButton); - }); - - await waitFor(() => { - expect(screen.queryByText("Resend Code (60s)")).toBeInTheDocument(); - }); - }); - - it("calls verifyCode when all 6 digits are entered", async () => { - const { verifyCode } = setup(); - - const inputs = screen.queryAllByRole("textbox"); - const user = userEvent.setup(); - - for (let i = 0; i < 6; i++) { - await user.click(inputs[i]); - await user.keyboard((i + 1).toString()); - } - - expect(verifyCode).toHaveBeenCalledWith("123456"); - }); - - it("calls verifyCode when OTP autofill injects all 6 digits into first input", async () => { - const { verifyCode } = setup(); - - const inputs = screen.queryAllByRole("textbox"); - await act(async () => { - fireEvent.change(inputs[0], { target: { value: "654321" } }); - }); - - expect(verifyCode).toHaveBeenCalledWith("654321"); - }); - - it("shows error notification when verify fails", async () => { - const verifyCode = vi.fn().mockRejectedValue(new Error("Invalid verification code")); - const { mockNotificator } = setup({ verifyCode }); - - const inputs = screen.queryAllByRole("textbox"); - await act(async () => { - fireEvent.change(inputs[0], { target: { value: "654321" } }); - }); - - await waitFor(() => { - expect(mockNotificator.error).toHaveBeenCalledWith("Invalid verification code"); - }); - }); - - function setup( - input: { - sendCode?: () => Promise; - verifyCode?: (code: string) => Promise; - } = {} - ) { - const sendCode = input.sendCode ?? vi.fn().mockResolvedValue(undefined); - const verifyCode = input.verifyCode ?? vi.fn().mockResolvedValue(undefined); - const mockNotificator = { success: vi.fn(), error: vi.fn() }; - const mockExtractErrorMessage = vi.fn((error: unknown) => (error instanceof Error ? error.message : "An error occurred. Please try again.")); - - const dependencies = { - useNotificator: () => mockNotificator, - extractErrorMessage: mockExtractErrorMessage - }; - - render(); - - return { sendCode, verifyCode, mockNotificator, mockExtractErrorMessage }; - } -}); diff --git a/apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/EmailVerificationStep.tsx b/apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/EmailVerificationStep.tsx deleted file mode 100644 index 5f5c4e5afb..0000000000 --- a/apps/deploy-web/src/components/onboarding/steps/EmailVerificationStep/EmailVerificationStep.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Spinner } from "@akashnetwork/ui/components"; -import { Mail, Refresh } from "iconoir-react"; - -import { Title } from "@src/components/shared/Title"; -import { useNotificator } from "@src/hooks/useNotificator"; -import type { AppError } from "@src/types"; -import { extractErrorMessage } from "@src/utils/errorUtils"; -import type { VerificationCodeInputRef } from "./VerificationCodeInput"; -import { VerificationCodeInput } from "./VerificationCodeInput"; - -const COOLDOWN_DURATION = 60; - -export const DEPENDENCIES = { - useNotificator, - // eslint-disable-next-line akash/dependencies-component-or-hook - extractErrorMessage -}; - -interface EmailVerificationStepProps { - sendCode: () => Promise; - verifyCode: (code: string) => Promise; - dependencies?: typeof DEPENDENCIES; -} - -export const EmailVerificationStep: React.FunctionComponent = ({ sendCode, verifyCode, dependencies: d = DEPENDENCIES }) => { - const notificator = d.useNotificator(); - - const [isResending, setIsResending] = useState(false); - const [isVerifying, setIsVerifying] = useState(false); - const [cooldownSeconds, setCooldownSeconds] = useState(0); - const codeInputRef = useRef(null); - - useEffect(() => { - if (cooldownSeconds <= 0) return; - - const timer = setTimeout(() => { - setCooldownSeconds(prev => prev - 1); - }, 1000); - - return () => clearTimeout(timer); - }, [cooldownSeconds]); - - const handleResendCode = useCallback(async () => { - setIsResending(true); - codeInputRef.current?.reset(); - - try { - await sendCode(); - setCooldownSeconds(COOLDOWN_DURATION); - notificator.success("Verification code sent. Please check your email for the 6-digit code."); - } catch (error) { - notificator.error(d.extractErrorMessage(error as AppError)); - } finally { - setIsResending(false); - } - }, [sendCode, notificator, d.extractErrorMessage]); - - const handleVerifyCode = useCallback( - async (code: string) => { - setIsVerifying(true); - - try { - await verifyCode(code); - notificator.success("Your email has been successfully verified"); - } catch (error) { - notificator.error(d.extractErrorMessage(error as AppError)); - setTimeout(() => codeInputRef.current?.reset(), 0); - } finally { - setIsVerifying(false); - } - }, - [verifyCode, notificator, d.extractErrorMessage] - ); - - return ( -
- Verify Your Email - - - -
-
- -
-
- Email Verification - We've sent a 6-digit verification code to your email address. -
- - - -

Didn't receive the code? Check your spam folder or request a new one.

- - -
-
-
- ); -}; diff --git a/apps/deploy-web/src/components/onboarding/steps/FreeTrialLandingStep/FreeTrialLandingStep.tsx b/apps/deploy-web/src/components/onboarding/steps/FreeTrialLandingStep/FreeTrialLandingStep.tsx deleted file mode 100644 index a9ac569048..0000000000 --- a/apps/deploy-web/src/components/onboarding/steps/FreeTrialLandingStep/FreeTrialLandingStep.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; -import React from "react"; -import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from "@akashnetwork/ui/components"; -import { Check, Cloud, Dollar, Rocket, Server } from "iconoir-react"; -import Link from "next/link"; - -import { AkashLogo } from "@src/components/layout/AkashLogo"; -import { Title } from "@src/components/shared/Title"; -import { useServices } from "@src/context/ServicesProvider"; -import { UrlService } from "@src/utils/urlUtils"; - -const benefits = [ - { - icon: