Skip to content

Commit 46004d4

Browse files
authored
feat(deployment): implements deploy button flow
refs #2470
1 parent 1245328 commit 46004d4

File tree

29 files changed

+1339
-149
lines changed

29 files changed

+1339
-149
lines changed

apps/deploy-web/src/components/auth/AuthPage/AuthPage.spec.tsx

Lines changed: 131 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,94 @@ describe(AuthPage.name, () => {
255255
});
256256
});
257257

258+
describe("when deploy button flow is active", () => {
259+
it("renders ConnectWalletButton when deploy button flow is active", () => {
260+
const ConnectWalletButtonMock = jest.fn(() => <button data-testid="connect-wallet-btn">Connect Wallet</button>);
261+
setup({
262+
searchParams: {
263+
tab: "login",
264+
from: "/new-deployment?repoUrl=https://github.com/test/repo.git"
265+
},
266+
dependencies: {
267+
useDeployButtonFlow: () => ({
268+
isDeployButtonFlow: true,
269+
params: {
270+
repoUrl: "https://github.com/test/repo.git",
271+
branch: null,
272+
buildCommand: null,
273+
startCommand: null,
274+
installCommand: null,
275+
buildDirectory: null,
276+
nodeVersion: null,
277+
templateId: null
278+
},
279+
buildReturnUrl: () => "/new-deployment",
280+
buildUrlParams: () => ({ repoUrl: "https://github.com/test/repo.git" })
281+
}),
282+
ConnectWalletButton: ConnectWalletButtonMock
283+
}
284+
});
285+
286+
expect(screen.queryByTestId("connect-wallet-btn")).toBeInTheDocument();
287+
});
288+
289+
it("does not render ConnectWalletButton when deploy button flow is inactive", () => {
290+
setup({
291+
searchParams: {
292+
tab: "login"
293+
}
294+
});
295+
296+
expect(screen.queryByTestId("connect-wallet-btn")).not.toBeInTheDocument();
297+
});
298+
299+
it("redirects to returnUrl when wallet connects during deploy button flow", async () => {
300+
const { router } = setup({
301+
searchParams: {
302+
tab: "login",
303+
from: "/new-deployment?repoUrl=https://github.com/test/repo.git"
304+
},
305+
dependencies: {
306+
useDeployButtonFlow: () => ({
307+
isDeployButtonFlow: true,
308+
params: {
309+
repoUrl: "https://github.com/test/repo.git",
310+
branch: null,
311+
buildCommand: null,
312+
startCommand: null,
313+
installCommand: null,
314+
buildDirectory: null,
315+
nodeVersion: null,
316+
templateId: null
317+
},
318+
buildReturnUrl: () => "/new-deployment?repoUrl=https://github.com/test/repo.git",
319+
buildUrlParams: () => ({ repoUrl: "https://github.com/test/repo.git" })
320+
}),
321+
useWallet: () => ({
322+
address: "akash1test",
323+
walletName: "test",
324+
isWalletConnected: true,
325+
isWalletLoaded: true,
326+
connectManagedWallet: jest.fn(),
327+
logout: jest.fn(),
328+
signAndBroadcastTx: jest.fn(),
329+
isManaged: false,
330+
isCustodial: false,
331+
isWalletLoading: false,
332+
isTrialing: false,
333+
isOnboarding: false,
334+
switchWalletType: jest.fn(),
335+
hasManagedWallet: false
336+
})
337+
}
338+
});
339+
340+
await waitFor(() => {
341+
expect(router.push).toHaveBeenCalledWith("/new-deployment?repoUrl=https://github.com/test/repo.git");
342+
});
343+
});
344+
});
345+
258346
function setup(input: {
259347
searchParams?: {
260348
tab?: "login" | "signup" | "forgot-password";
@@ -269,16 +357,50 @@ describe(AuthPage.name, () => {
269357
replace: jest.fn(url => {
270358
setRouterPageParams?.(new URL(url as string, "http://localunittest:8080").searchParams);
271359
return Promise.resolve(true);
272-
})
360+
}),
361+
push: jest.fn().mockResolvedValue(true)
273362
});
274363
const checkSession = jest.fn(async () => undefined);
275-
const useUserMock: typeof DEPENDENCIES.useUser = () => ({
364+
const useUser: typeof DEPENDENCIES.useUser = () => ({
276365
checkSession,
277366
isLoading: false,
278367
error: undefined,
279368
user: {}
280369
});
281370

371+
const useDeployButtonFlow: typeof DEPENDENCIES.useDeployButtonFlow = () => ({
372+
isDeployButtonFlow: false,
373+
params: {
374+
repoUrl: null,
375+
branch: null,
376+
buildCommand: null,
377+
startCommand: null,
378+
installCommand: null,
379+
buildDirectory: null,
380+
nodeVersion: null,
381+
templateId: null
382+
},
383+
buildReturnUrl: () => "/",
384+
buildUrlParams: () => ({})
385+
});
386+
387+
const useWallet: typeof DEPENDENCIES.useWallet = () => ({
388+
address: "",
389+
walletName: "",
390+
isWalletConnected: false,
391+
isWalletLoaded: false,
392+
connectManagedWallet: jest.fn(),
393+
logout: jest.fn(),
394+
signAndBroadcastTx: jest.fn(),
395+
isManaged: false,
396+
isCustodial: false,
397+
isWalletLoading: false,
398+
isTrialing: false,
399+
isOnboarding: false,
400+
switchWalletType: jest.fn(),
401+
hasManagedWallet: false
402+
});
403+
282404
const params = new URLSearchParams();
283405
params.set("tab", input.searchParams?.tab || "login");
284406
if (input.searchParams?.from) {
@@ -287,13 +409,13 @@ describe(AuthPage.name, () => {
287409
if (input.searchParams?.returnTo) {
288410
params.set("returnTo", input.searchParams.returnTo);
289411
}
290-
const useSearchParamsMock = () => {
412+
const useSearchParams = () => {
291413
const [pageParams, setPageParams] = useState(params);
292414
setRouterPageParams = setPageParams;
293415
return pageParams as ReadonlyURLSearchParams;
294416
};
295417

296-
const TurnstileMock = jest.fn(({ turnstileRef }: { turnstileRef?: RefObject<TurnstileRef> }) => {
418+
const Turnstile = jest.fn(({ turnstileRef }: { turnstileRef?: RefObject<TurnstileRef> }) => {
297419
if (turnstileRef) {
298420
(turnstileRef as { current: TurnstileRef }).current = {
299421
renderAndWaitResponse: jest.fn().mockResolvedValue({ token: "test-captcha-token" })
@@ -307,10 +429,12 @@ describe(AuthPage.name, () => {
307429
<AuthPage
308430
dependencies={{
309431
...MockComponents(DEPENDENCIES),
310-
useUser: useUserMock,
311-
useSearchParams: useSearchParamsMock,
432+
useUser,
433+
useSearchParams,
312434
useRouter: () => router,
313-
Turnstile: TurnstileMock,
435+
useDeployButtonFlow: input.dependencies?.useDeployButtonFlow || useDeployButtonFlow,
436+
useWallet: input.dependencies?.useWallet || useWallet,
437+
Turnstile,
314438
...input.dependencies
315439
}}
316440
/>

apps/deploy-web/src/components/auth/AuthPage/AuthPage.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useCallback, useRef, useState } from "react";
3+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
44
import { Separator, Tabs, TabsContent, TabsList, TabsTrigger } from "@akashnetwork/ui/components";
55
import { useMutation } from "@tanstack/react-query";
66
import { DollarSignIcon, RocketIcon, ZapIcon } from "lucide-react";
@@ -12,7 +12,10 @@ import { AkashConsoleLogo } from "@src/components/icons/AkashConsoleLogo";
1212
import { RemoteApiError } from "@src/components/shared/RemoteApiError/RemoteApiError";
1313
import type { TurnstileRef } from "@src/components/turnstile/Turnstile";
1414
import { ClientOnlyTurnstile } from "@src/components/turnstile/Turnstile";
15+
import { ConnectWalletButton } from "@src/components/wallet/ConnectWalletButton";
1516
import { useServices } from "@src/context/ServicesProvider";
17+
import { useWallet as useWalletOriginal } from "@src/context/WalletProvider";
18+
import { useDeployButtonFlow } from "@src/hooks/useDeployButtonFlow/useDeployButtonFlow";
1619
import { useUser } from "@src/hooks/useUser";
1720
import { AuthLayout } from "../AuthLayout/AuthLayout";
1821
import { ForgotPasswordForm } from "../ForgotPasswordForm/ForgotPasswordForm";
@@ -42,7 +45,10 @@ export const DEPENDENCIES = {
4245
Separator,
4346
useUser,
4447
useSearchParams,
45-
useRouter
48+
useRouter,
49+
ConnectWalletButton,
50+
useDeployButtonFlow,
51+
useWallet: useWalletOriginal
4652
};
4753

4854
interface Props {
@@ -54,16 +60,28 @@ export function AuthPage({ dependencies: d = DEPENDENCIES }: Props = {}) {
5460
const router = d.useRouter();
5561
const searchParams = d.useSearchParams();
5662
const { checkSession } = d.useUser();
63+
const { isWalletConnected } = d.useWallet();
5764
const [email, setEmail] = useState("");
5865
const turnstileRef = useRef<TurnstileRef | null>(null);
66+
const { isDeployButtonFlow } = d.useDeployButtonFlow();
67+
const returnUrl = useMemo(() => searchParams.get("from") || searchParams.get("returnTo") || "/", [searchParams]);
68+
69+
useEffect(
70+
function redirectToDeployForCustodialWallet() {
71+
if (isWalletConnected && isDeployButtonFlow && returnUrl) {
72+
router.push(returnUrl);
73+
}
74+
},
75+
[isWalletConnected, isDeployButtonFlow, returnUrl, router]
76+
);
5977

6078
const redirectToSocialLogin = useCallback(
6179
async (provider: "github" | "google-oauth2") => {
62-
const returnUrl = searchParams.get("from") || searchParams.get("returnTo") || "/";
6380
await authService.loginViaOauth({ returnTo: returnUrl, connection: provider });
6481
},
65-
[searchParams]
82+
[authService, returnUrl]
6683
);
84+
6785
const signInOrSignUp = useMutation({
6886
async mutationFn(input: Tagged<"signin", SignInFormValues> | Tagged<"signup", SignUpFormValues>) {
6987
if (!turnstileRef.current) {
@@ -94,6 +112,7 @@ export function AuthPage({ dependencies: d = DEPENDENCIES }: Props = {}) {
94112
},
95113
[searchParams, router]
96114
);
115+
97116
const forgotPassword = useMutation({
98117
async mutationFn(input: { email: string }) {
99118
if (!turnstileRef.current) {
@@ -103,6 +122,7 @@ export function AuthPage({ dependencies: d = DEPENDENCIES }: Props = {}) {
103122
await authService.sendPasswordResetEmail({ email: input.email, captchaToken });
104123
}
105124
});
125+
106126
const resetMutations = useCallback(() => {
107127
signInOrSignUp.reset();
108128
forgotPassword.reset();
@@ -209,6 +229,23 @@ export function AuthPage({ dependencies: d = DEPENDENCIES }: Props = {}) {
209229

210230
<d.SocialAuth onSocialLogin={redirectToSocialLogin} />
211231

232+
{isDeployButtonFlow && (
233+
<>
234+
<div className="relative flex items-center justify-center self-stretch py-2.5">
235+
<d.Separator className="absolute inset-0 top-1/2" />
236+
<div className="current relative top-[-1px] z-10 px-2" style={{ backgroundColor: "hsl(var(--background))" }}>
237+
<span className="relative top-1/2 text-xs font-normal text-neutral-500 dark:text-neutral-400">Or</span>
238+
</div>
239+
</div>
240+
<div className="mb-5">
241+
<d.ConnectWalletButton className="w-full" />
242+
<p className="mt-2 text-center text-xs text-neutral-500 dark:text-neutral-400">
243+
Connect a custodial wallet (Keplr, Leap) to skip authentication
244+
</p>
245+
</div>
246+
</>
247+
)}
248+
212249
<div className="relative flex items-center justify-center self-stretch py-2.5">
213250
<d.Separator className="absolute inset-0 top-1/2" />
214251
<div className="current relative top-[-1px] z-10 px-2" style={{ backgroundColor: "hsl(var(--background))" }}>

apps/deploy-web/src/components/deployments/DeploymentDetail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { useWhen } from "@src/hooks/useWhen";
2323
import { useDeploymentDetail } from "@src/queries/useDeploymentQuery";
2424
import { useDeploymentLeaseList } from "@src/queries/useLeaseQuery";
2525
import { useProviderList } from "@src/queries/useProvidersQuery";
26-
import { extractRepositoryUrl, isCiCdImageInYaml } from "@src/services/remote-deploy/remote-deployment-controller.service";
26+
import { extractRepositoryUrl, isCiCdImageInYaml } from "@src/services/remote-deploy/env-var-manager.service";
2727
import { RouteStep } from "@src/types/route-steps.type";
2828
import { UrlService } from "@src/utils/urlUtils";
2929
import Layout from "../layout/Layout";

apps/deploy-web/src/components/layout/LoadingBlocker/LoadingBlocker.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import type { FCWithChildren } from "@src/types/component";
66
interface LoadingBlockerProps {
77
isLoading: boolean;
88
testId?: string;
9+
text?: string;
910
}
1011

11-
export const LoadingBlocker: FCWithChildren<LoadingBlockerProps> = ({ children, isLoading, testId }) => {
12-
return isLoading ? <Loading text="" testId={testId} /> : <>{children}</>;
12+
export const LoadingBlocker: FCWithChildren<LoadingBlockerProps> = ({ children, isLoading, testId, text }) => {
13+
return isLoading ? <Loading text={text || ""} testId={testId} /> : <>{children}</>;
1314
};

apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,7 @@ export const ManifestEdit: React.FunctionComponent<Props> = ({
104104
if (selectedTemplate?.name) {
105105
setDeploymentName(selectedTemplate.name);
106106
}
107-
108-
// eslint-disable-next-line react-hooks/exhaustive-deps
109-
}, []);
107+
}, [selectedTemplate]);
110108

111109
useEffect(() => {
112110
const timer = Timer(500);

0 commit comments

Comments
 (0)