|
| 1 | +diff --git a/packages/remote-web/src/pages/InvitationPage.tsx b/packages/remote-web/src/pages/InvitationPage.tsx |
| 2 | +index ae9b23f1c..466e42741 100644 |
| 3 | +--- a/packages/remote-web/src/pages/InvitationPage.tsx |
| 4 | ++++ b/packages/remote-web/src/pages/InvitationPage.tsx |
| 5 | +@@ -1,11 +1,13 @@ |
| 6 | + import { useEffect, useState } from "react"; |
| 7 | + import { useParams } from "@tanstack/react-router"; |
| 8 | + import { |
| 9 | ++ getAuthMethods, |
| 10 | + getInvitation, |
| 11 | + initOAuth, |
| 12 | + type InvitationLookupResponse, |
| 13 | + type OAuthProvider, |
| 14 | + } from "@remote/shared/lib/api"; |
| 15 | ++import { useQuery } from "@tanstack/react-query"; |
| 16 | + import { |
| 17 | + generateChallenge, |
| 18 | + generateVerifier, |
| 19 | +@@ -22,6 +24,18 @@ export default function InvitationPage() { |
| 20 | + const [pendingProvider, setPendingProvider] = useState<OAuthProvider | null>( |
| 21 | + null, |
| 22 | + ); |
| 23 | ++ const { |
| 24 | ++ data: authMethods, |
| 25 | ++ error: authMethodsError, |
| 26 | ++ isError: isAuthMethodsError, |
| 27 | ++ } = useQuery({ |
| 28 | ++ queryKey: ["remote-auth-methods"], |
| 29 | ++ queryFn: getAuthMethods, |
| 30 | ++ staleTime: 60_000, |
| 31 | ++ }); |
| 32 | ++ const oauthProviders = (authMethods?.oauth_providers ?? []).filter( |
| 33 | ++ isSupportedOAuthProvider, |
| 34 | ++ ); |
| 35 | + |
| 36 | + useEffect(() => { |
| 37 | + let cancelled = false; |
| 38 | +@@ -129,24 +143,30 @@ export default function InvitationPage() { |
| 39 | + <p className="text-sm text-high">{error}</p> |
| 40 | + </div> |
| 41 | + )} |
| 42 | ++ {isAuthMethodsError && ( |
| 43 | ++ <div className="rounded-sm border border-error/30 bg-error/10 p-base"> |
| 44 | ++ <p className="text-sm text-high"> |
| 45 | ++ {authMethodsError instanceof Error |
| 46 | ++ ? authMethodsError.message |
| 47 | ++ : "Failed to load available sign-in methods."} |
| 48 | ++ </p> |
| 49 | ++ </div> |
| 50 | ++ )} |
| 51 | + |
| 52 | + <section className="space-y-base border-t border-border pt-base text-center"> |
| 53 | + <p className="text-sm text-low">Choose a provider to continue:</p> |
| 54 | + <div className="flex flex-col items-center gap-2"> |
| 55 | +- <OAuthButton |
| 56 | +- provider="github" |
| 57 | +- label="Continue with GitHub" |
| 58 | +- onClick={() => void handleOAuthLogin("github")} |
| 59 | +- disabled={pendingProvider !== null} |
| 60 | +- loading={pendingProvider === "github"} |
| 61 | +- /> |
| 62 | +- <OAuthButton |
| 63 | +- provider="google" |
| 64 | +- label="Continue with Google" |
| 65 | +- onClick={() => void handleOAuthLogin("google")} |
| 66 | +- disabled={pendingProvider !== null} |
| 67 | +- loading={pendingProvider === "google"} |
| 68 | +- /> |
| 69 | ++ {!isAuthMethodsError && |
| 70 | ++ oauthProviders.map((provider) => ( |
| 71 | ++ <OAuthButton |
| 72 | ++ key={provider} |
| 73 | ++ provider={provider} |
| 74 | ++ label={`Continue with ${providerLabel(provider)}`} |
| 75 | ++ onClick={() => void handleOAuthLogin(provider)} |
| 76 | ++ disabled={pendingProvider !== null} |
| 77 | ++ loading={pendingProvider === provider} |
| 78 | ++ /> |
| 79 | ++ ))} |
| 80 | + </div> |
| 81 | + </section> |
| 82 | + </div> |
| 83 | +@@ -155,6 +175,23 @@ export default function InvitationPage() { |
| 84 | + ); |
| 85 | + } |
| 86 | + |
| 87 | ++const SUPPORTED_OAUTH_PROVIDERS: OAuthProvider[] = ["github", "google", "zoho"]; |
| 88 | ++ |
| 89 | ++function isSupportedOAuthProvider(provider: string): provider is OAuthProvider { |
| 90 | ++ return SUPPORTED_OAUTH_PROVIDERS.includes(provider as OAuthProvider); |
| 91 | ++} |
| 92 | ++ |
| 93 | ++function providerLabel(provider: OAuthProvider): string { |
| 94 | ++ switch (provider) { |
| 95 | ++ case "github": |
| 96 | ++ return "GitHub"; |
| 97 | ++ case "google": |
| 98 | ++ return "Google"; |
| 99 | ++ case "zoho": |
| 100 | ++ return "Zoho"; |
| 101 | ++ } |
| 102 | ++} |
| 103 | ++ |
| 104 | + function OAuthButton({ |
| 105 | + provider, |
| 106 | + label, |
| 107 | +@@ -176,9 +213,7 @@ function OAuthButton({ |
| 108 | + onClick={onClick} |
| 109 | + disabled={disabled || loading} |
| 110 | + > |
| 111 | +- {loading |
| 112 | +- ? `Opening ${provider === "github" ? "GitHub" : "Google"}...` |
| 113 | +- : label} |
| 114 | ++ {loading ? `Opening ${providerLabel(provider)}...` : label} |
| 115 | + </button> |
| 116 | + ); |
| 117 | + } |
| 118 | +diff --git a/packages/remote-web/src/pages/LoginPage.tsx b/packages/remote-web/src/pages/LoginPage.tsx |
| 119 | +index 9446e0ed0..b9369efda 100644 |
| 120 | +--- a/packages/remote-web/src/pages/LoginPage.tsx |
| 121 | ++++ b/packages/remote-web/src/pages/LoginPage.tsx |
| 122 | +@@ -34,7 +34,9 @@ export default function LoginPage() { |
| 123 | + }); |
| 124 | + |
| 125 | + const hasLocalAuth = authMethods?.local_auth_enabled ?? false; |
| 126 | +- const oauthProviders = authMethods?.oauth_providers ?? []; |
| 127 | ++ const oauthProviders = (authMethods?.oauth_providers ?? []).filter( |
| 128 | ++ isSupportedOAuthProvider, |
| 129 | ++ ); |
| 130 | + const hasOAuthProviders = oauthProviders.length > 0; |
| 131 | + |
| 132 | + const handleLogin = async (provider: OAuthProvider) => { |
| 133 | +@@ -152,27 +154,16 @@ export default function LoginPage() { |
| 134 | + |
| 135 | + <div className="flex flex-col items-center gap-2"> |
| 136 | + {!isAuthMethodsError && |
| 137 | +- hasOAuthProviders && |
| 138 | +- oauthProviders.includes("github") && ( |
| 139 | +- <OAuthButton |
| 140 | +- provider="github" |
| 141 | +- label="Continue with GitHub" |
| 142 | +- onClick={() => void handleLogin("github")} |
| 143 | +- disabled={pending !== null} |
| 144 | +- loading={pending === "github"} |
| 145 | +- /> |
| 146 | +- )} |
| 147 | +- {!isAuthMethodsError && |
| 148 | +- hasOAuthProviders && |
| 149 | +- oauthProviders.includes("google") && ( |
| 150 | ++ oauthProviders.map((provider) => ( |
| 151 | + <OAuthButton |
| 152 | +- provider="google" |
| 153 | +- label="Continue with Google" |
| 154 | +- onClick={() => void handleLogin("google")} |
| 155 | ++ key={provider} |
| 156 | ++ provider={provider} |
| 157 | ++ label={`Continue with ${providerLabel(provider)}`} |
| 158 | ++ onClick={() => void handleLogin(provider)} |
| 159 | + disabled={pending !== null} |
| 160 | +- loading={pending === "google"} |
| 161 | ++ loading={pending === provider} |
| 162 | + /> |
| 163 | +- )} |
| 164 | ++ ))} |
| 165 | + </div> |
| 166 | + </section> |
| 167 | + |
| 168 | +@@ -193,6 +184,23 @@ export default function LoginPage() { |
| 169 | + ); |
| 170 | + } |
| 171 | + |
| 172 | ++const SUPPORTED_OAUTH_PROVIDERS: OAuthProvider[] = ["github", "google", "zoho"]; |
| 173 | ++ |
| 174 | ++function isSupportedOAuthProvider(provider: string): provider is OAuthProvider { |
| 175 | ++ return SUPPORTED_OAUTH_PROVIDERS.includes(provider as OAuthProvider); |
| 176 | ++} |
| 177 | ++ |
| 178 | ++function providerLabel(provider: OAuthProvider): string { |
| 179 | ++ switch (provider) { |
| 180 | ++ case "github": |
| 181 | ++ return "GitHub"; |
| 182 | ++ case "google": |
| 183 | ++ return "Google"; |
| 184 | ++ case "zoho": |
| 185 | ++ return "Zoho"; |
| 186 | ++ } |
| 187 | ++} |
| 188 | ++ |
| 189 | + function OAuthButton({ |
| 190 | + provider, |
| 191 | + label, |
| 192 | +@@ -214,9 +222,7 @@ function OAuthButton({ |
| 193 | + onClick={onClick} |
| 194 | + disabled={disabled || loading} |
| 195 | + > |
| 196 | +- {loading |
| 197 | +- ? `Opening ${provider === "github" ? "GitHub" : "Google"}...` |
| 198 | +- : label} |
| 199 | ++ {loading ? `Opening ${providerLabel(provider)}...` : label} |
| 200 | + </button> |
| 201 | + ); |
| 202 | + } |
| 203 | +diff --git a/packages/remote-web/src/shared/lib/api.ts b/packages/remote-web/src/shared/lib/api.ts |
| 204 | +index feebf0cb4..f8910a66c 100644 |
| 205 | +--- a/packages/remote-web/src/shared/lib/api.ts |
| 206 | ++++ b/packages/remote-web/src/shared/lib/api.ts |
| 207 | +@@ -5,7 +5,7 @@ import type { ListOrganizationsResponse } from "shared/types"; |
| 208 | + |
| 209 | + const API_BASE = import.meta.env.VITE_API_BASE_URL || ""; |
| 210 | + |
| 211 | +-export type OAuthProvider = "github" | "google"; |
| 212 | ++export type OAuthProvider = "github" | "google" | "zoho"; |
| 213 | + |
| 214 | + export type AuthMethodsResponse = { |
| 215 | + local_auth_enabled: boolean; |
| 216 | +diff --git a/packages/web-core/src/features/onboarding/ui/OnboardingSignInPage.tsx b/packages/web-core/src/features/onboarding/ui/OnboardingSignInPage.tsx |
| 217 | +index 3e9b5fd70..8c4454228 100644 |
| 218 | +--- a/packages/web-core/src/features/onboarding/ui/OnboardingSignInPage.tsx |
| 219 | ++++ b/packages/web-core/src/features/onboarding/ui/OnboardingSignInPage.tsx |
| 220 | +@@ -61,7 +61,22 @@ type SignInCompletionMethod = |
| 221 | + | 'auth_dialog' |
| 222 | + | 'local_auth' |
| 223 | + | 'oauth_github' |
| 224 | +- | 'oauth_google'; |
| 225 | ++ | 'oauth_google' |
| 226 | ++ | 'oauth_zoho'; |
| 227 | ++ |
| 228 | ++function completionMethodForProvider( |
| 229 | ++ provider: OAuthProvider |
| 230 | ++): SignInCompletionMethod { |
| 231 | ++ switch (provider) { |
| 232 | ++ case 'github': |
| 233 | ++ return 'oauth_github'; |
| 234 | ++ case 'google': |
| 235 | ++ return 'oauth_google'; |
| 236 | ++ case 'zoho': |
| 237 | ++ return 'oauth_zoho'; |
| 238 | ++ } |
| 239 | ++} |
| 240 | ++ |
| 241 | + function resolveTheme(theme: ThemeMode): 'light' | 'dark' { |
| 242 | + if (theme === ThemeMode.SYSTEM) { |
| 243 | + return window.matchMedia('(prefers-color-scheme: dark)').matches |
| 244 | +@@ -98,8 +113,9 @@ export function OnboardingSignInPage() { |
| 245 | + staleTime: 60_000, |
| 246 | + }); |
| 247 | + const hasLocalAuth = authMethods?.local_auth_enabled ?? false; |
| 248 | +- const oauthProviders = authMethods?.oauth_providers ?? []; |
| 249 | +- const hasOAuthProviders = oauthProviders.length > 0; |
| 250 | ++ const oauthProviders = (authMethods?.oauth_providers ?? []).filter( |
| 251 | ++ isSupportedOAuthProvider |
| 252 | ++ ); |
| 253 | + |
| 254 | + const trackRemoteOnboardingEvent = useCallback( |
| 255 | + (eventName: string, properties: Record<string, unknown> = {}) => { |
| 256 | +@@ -226,7 +242,7 @@ export function OnboardingSignInPage() { |
| 257 | + |
| 258 | + if (didSignIn) { |
| 259 | + await finishOnboarding({ |
| 260 | +- method: provider === 'github' ? 'oauth_github' : 'oauth_google', |
| 261 | ++ method: completionMethodForProvider(provider), |
| 262 | + }); |
| 263 | + } |
| 264 | + }; |
| 265 | +@@ -332,24 +348,16 @@ export function OnboardingSignInPage() { |
| 266 | + /> |
| 267 | + ) : !isAuthMethodsError ? ( |
| 268 | + <> |
| 269 | +- {hasOAuthProviders && oauthProviders.includes('github') && ( |
| 270 | +- <OAuthSignInButton |
| 271 | +- provider="github" |
| 272 | +- onClick={() => void handleProviderSignIn('github')} |
| 273 | +- disabled={saving || pendingProvider !== null} |
| 274 | +- loading={pendingProvider === 'github'} |
| 275 | +- loadingText="Opening GitHub..." |
| 276 | +- /> |
| 277 | +- )} |
| 278 | +- {hasOAuthProviders && oauthProviders.includes('google') && ( |
| 279 | ++ {oauthProviders.map((provider) => ( |
| 280 | + <OAuthSignInButton |
| 281 | +- provider="google" |
| 282 | +- onClick={() => void handleProviderSignIn('google')} |
| 283 | ++ key={provider} |
| 284 | ++ provider={provider} |
| 285 | ++ onClick={() => void handleProviderSignIn(provider)} |
| 286 | + disabled={saving || pendingProvider !== null} |
| 287 | +- loading={pendingProvider === 'google'} |
| 288 | +- loadingText="Opening Google..." |
| 289 | ++ loading={pendingProvider === provider} |
| 290 | ++ loadingText={`Opening ${providerLabel(provider)}...`} |
| 291 | + /> |
| 292 | +- )} |
| 293 | ++ ))} |
| 294 | + </> |
| 295 | + ) : null} |
| 296 | + </section> |
| 297 | +@@ -475,3 +483,20 @@ export function OnboardingSignInPage() { |
| 298 | + </div> |
| 299 | + ); |
| 300 | + } |
| 301 | ++ |
| 302 | ++const SUPPORTED_OAUTH_PROVIDERS: OAuthProvider[] = ['github', 'google', 'zoho']; |
| 303 | ++ |
| 304 | ++function isSupportedOAuthProvider(provider: string): provider is OAuthProvider { |
| 305 | ++ return SUPPORTED_OAUTH_PROVIDERS.includes(provider as OAuthProvider); |
| 306 | ++} |
| 307 | ++ |
| 308 | ++function providerLabel(provider: OAuthProvider): string { |
| 309 | ++ switch (provider) { |
| 310 | ++ case 'github': |
| 311 | ++ return 'GitHub'; |
| 312 | ++ case 'google': |
| 313 | ++ return 'Google'; |
| 314 | ++ case 'zoho': |
| 315 | ++ return 'Zoho'; |
| 316 | ++ } |
| 317 | ++} |
0 commit comments