diff --git a/apps/docs/content/guides/auth/auth-identity-linking.mdx b/apps/docs/content/guides/auth/auth-identity-linking.mdx index aa8a98b22ec44..fe950304c1d5a 100644 --- a/apps/docs/content/guides/auth/auth-identity-linking.mdx +++ b/apps/docs/content/guides/auth/auth-identity-linking.mdx @@ -90,6 +90,63 @@ response = supabase.auth.link_identity({'provider': 'google'}) In the example above, the user will be redirected to Google to complete the OAuth2.0 flow. Once the OAuth2.0 flow has completed successfully, the user will be redirected back to the application and the Google identity will be linked to the user. You can enable manual linking from your project's authentication [configuration options](/dashboard/project/_/auth/providers) or by setting the environment variable `GOTRUE_SECURITY_MANUAL_LINKING_ENABLED: true` when self-hosting. +### Link identity with native OAuth (ID token) + + + + +For native mobile applications, you can link an identity using an ID token obtained from a third-party OAuth provider. This is useful when you want to use native OAuth flows (like Google Sign-In or Sign in with Apple) rather than web-based OAuth redirects. + +```js +// Example with Google Sign-In (using a native Google Sign-In library) +const idToken = 'ID_TOKEN_FROM_GOOGLE' +const accessToken = 'ACCESS_TOKEN_FROM_GOOGLE' + +const { data, error } = await supabase.auth.linkIdentityWithIdToken({ + provider: 'google', + token: idToken, + access_token: accessToken, +}) +``` + + +<$Show if="sdk:dart"> + + +For Flutter applications, you can link an identity using an ID token obtained from native OAuth packages like `google_sign_in` or `sign_in_with_apple`. Call [`linkIdentityWithIdToken()`](/docs/reference/dart/auth-linkidentitywithidtoken): + +```dart +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +// First, obtain the ID token from the native provider +final GoogleSignIn googleSignIn = GoogleSignIn( + clientId: iosClientId, + serverClientId: webClientId, +); +final googleUser = await googleSignIn.signIn(); +final googleAuth = await googleUser!.authentication; + +// Link the Google identity to the current user +final response = await supabase.auth.linkIdentityWithIdToken( + provider: OAuthProvider.google, + idToken: googleAuth.idToken!, + accessToken: googleAuth.accessToken!, +); +``` + +This method supports the same OAuth providers as `signInWithIdToken()`: Google, Apple, Facebook, Kakao, and Keycloak. + + + + + ## Unlink an identity ` headers for row limit enforcement. + - Only use with UPDATE (PATCH), DELETE operations, or RPC calls that modify data. + - If the query would affect more rows than specified, PostgREST will return an error. + - This is useful for preventing accidental bulk updates or deletes. + examples: + - id: with-update + name: With `update()` + code: | + ```swift + try await supabase + .from("users") + .update(["status": "active"]) + .eq("id", value: 1) + .maxAffected(1) + .execute() + ``` + description: | + Ensure that only one row is updated. If the query would update more than one row, an error is returned. + isSpotlight: true + - id: with-delete + name: With `delete()` + code: | + ```swift + try await supabase + .from("users") + .delete() + .in("id", values: [1, 2, 3]) + .maxAffected(3) + .execute() + ``` + description: | + Ensure that only three rows are deleted. If the query would delete more than three rows, an error is returned. + - id: with-rpc + name: With `rpc()` + code: | + ```swift + try await supabase + .rpc("delete_inactive_users") + .maxAffected(10) + .execute() + ``` + description: | + Ensure that the RPC call affects at most 10 rows. Useful for limiting the impact of stored procedures. + - id: single title: single() description: | diff --git a/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx index 26bd90de84dbf..f52b241cb08bf 100644 --- a/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx @@ -7,6 +7,7 @@ import * as z from 'zod' import { useParams } from 'common' import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' import { StringNumberOrNull } from 'components/ui/Forms/Form.constants' import NoPermission from 'components/ui/NoPermission' import UpgradeToPro from 'components/ui/UpgradeToPro' @@ -16,9 +17,6 @@ import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/constants' import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, Button, Card, CardContent, @@ -28,8 +26,8 @@ import { Form_Shadcn_, Input_Shadcn_, PrePostTab, - WarningIcon, } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' const FormSchema = z.object({ @@ -55,7 +53,12 @@ export const AdvancedAuthSettingsForm = () => { const [isUpdatingRequestDurationForm, setIsUpdatingRequestDurationForm] = useState(false) const [isUpdatingDatabaseForm, setIsUpdatingDatabaseForm] = useState(false) - const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) + const { + data: authConfig, + error: authConfigError, + isError, + isLoading, + } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() @@ -148,16 +151,26 @@ export const AdvancedAuthSettingsForm = () => { if (isError) { return ( - - - Failed to retrieve auth configuration - {authConfigError.message} - + + + ) } if (!canReadConfig) { - return + return ( + + + + ) + } + + if (isLoading) { + return ( + + + + ) } return ( diff --git a/apps/studio/components/interfaces/Auth/AuditLogsForm.tsx b/apps/studio/components/interfaces/Auth/AuditLogsForm.tsx index b3e9ccded3448..1630654eae40a 100644 --- a/apps/studio/components/interfaces/Auth/AuditLogsForm.tsx +++ b/apps/studio/components/interfaces/Auth/AuditLogsForm.tsx @@ -7,6 +7,7 @@ import { boolean, object } from 'yup' import { useParams } from 'common' import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' import { InlineLink } from 'components/ui/InlineLink' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' @@ -14,9 +15,6 @@ import { useTablesQuery } from 'data/tables/tables-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, Button, Card, CardContent, @@ -25,9 +23,8 @@ import { FormField_Shadcn_, Form_Shadcn_, Switch, - WarningIcon, } from 'ui' -import { Admonition } from 'ui-patterns' +import { Admonition, GenericSkeletonLoader } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' const schema = object({ @@ -53,7 +50,12 @@ export const AuditLogsForm = () => { }) const auditLogTable = tables.find((x) => x.name === AUDIT_LOG_ENTRIES_TABLE) - const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) + const { + data: authConfig, + error: authConfigError, + isError, + isLoading, + } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation({ onError: (error) => { @@ -85,11 +87,20 @@ export const AuditLogsForm = () => { if (isError) { return ( - - - Failed to retrieve auth configuration - {authConfigError.message} - + + + + ) + } + + if (isLoading) { + return ( + + + ) } diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx index 65adb7ac94740..178a94789a61c 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx @@ -7,10 +7,10 @@ import { ScaffoldSectionDescription, ScaffoldSectionTitle, } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' import { ResourceList } from 'components/ui/Resource/ResourceList' import { HorizontalShimmerWithIcon } from 'components/ui/Shimmers/Shimmers' import { useAuthConfigQuery } from 'data/auth/auth-config-query' -import { DOCS_URL } from 'lib/constants' import { Alert_Shadcn_, AlertDescription_Shadcn_, @@ -39,79 +39,82 @@ export const AuthProvidersForm = () => { Authenticate your users through a suite of providers and login methods -
- {authConfig?.EXTERNAL_EMAIL_ENABLED && authConfig?.MAILER_OTP_EXP > 3600 && ( - - -
- OTP expiry exceeds recommended threshold - -

- We have detected that you have enabled the email provider with the OTP expiry set - to more than an hour. It is recommended to set this value to less than an hour. -

- -
-
-
- )} - - {isLoading && - PROVIDERS_SCHEMAS.map((provider) => ( -
- -
- ))} - {isSuccess && - PROVIDERS_SCHEMAS.map((provider) => { - const providerSchema = - provider.title === 'Phone' - ? { ...provider, validationSchema: getPhoneProviderValidationSchema(authConfig) } - : provider - let isActive = false - if (providerSchema.title === 'SAML 2.0') { - isActive = authConfig && (authConfig as any)['SAML_ENABLED'] - } else if (providerSchema.title === 'LinkedIn (OIDC)') { - isActive = authConfig && (authConfig as any)['EXTERNAL_LINKEDIN_OIDC_ENABLED'] - } else if (providerSchema.title === 'Slack (OIDC)') { - isActive = authConfig && (authConfig as any)['EXTERNAL_SLACK_OIDC_ENABLED'] - } else if (providerSchema.title.includes('Web3')) { - isActive = authConfig && (authConfig as any)['EXTERNAL_WEB3_SOLANA_ENABLED'] - } else { - isActive = - authConfig && - (authConfig as any)[`EXTERNAL_${providerSchema.title.toUpperCase()}_ENABLED`] - } - return ( - - ) - })} - {isError && ( - + {isError ? ( + + ) : ( +
+ {authConfig?.EXTERNAL_EMAIL_ENABLED && authConfig?.MAILER_OTP_EXP > 3600 && ( + - Failed to retrieve auth configuration - - {(authConfigError as any)?.message} - +
+ OTP expiry exceeds recommended threshold + +

+ We have detected that you have enabled the email provider with the OTP expiry + set to more than an hour. It is recommended to set this value to less than an + hour. +

+ +
+
)} - -
+ + + {isLoading && + PROVIDERS_SCHEMAS.map((provider) => ( +
+ +
+ ))} + {isSuccess && + PROVIDERS_SCHEMAS.map((provider) => { + const providerSchema = + provider.title === 'Phone' + ? { + ...provider, + validationSchema: getPhoneProviderValidationSchema(authConfig), + } + : provider + let isActive = false + if (providerSchema.title === 'SAML 2.0') { + isActive = authConfig && (authConfig as any)['SAML_ENABLED'] + } else if (providerSchema.title === 'LinkedIn (OIDC)') { + isActive = authConfig && (authConfig as any)['EXTERNAL_LINKEDIN_OIDC_ENABLED'] + } else if (providerSchema.title === 'Slack (OIDC)') { + isActive = authConfig && (authConfig as any)['EXTERNAL_SLACK_OIDC_ENABLED'] + } else if (providerSchema.title.includes('Web3')) { + isActive = authConfig && (authConfig as any)['EXTERNAL_WEB3_SOLANA_ENABLED'] + } else { + isActive = + authConfig && + (authConfig as any)[`EXTERNAL_${providerSchema.title.toUpperCase()}_ENABLED`] + } + return ( + + ) + })} +
+
+ )} ) } diff --git a/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx index 3717cad6b9ed7..3bdb420931859 100644 --- a/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx @@ -9,6 +9,7 @@ import { boolean, object, string } from 'yup' import { useParams } from 'common' import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' import { InlineLink } from 'components/ui/InlineLink' import NoPermission from 'components/ui/NoPermission' import { useAuthConfigQuery } from 'data/auth/auth-config-query' @@ -117,11 +118,10 @@ export const BasicAuthSettingsForm = () => { User Signups {isError && ( - - - Failed to retrieve auth configuration - {authConfigError.message} - + )} {isPermissionsLoaded && !canReadConfig && ( diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx index 82875d1ff7b09..71b8164e6948e 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx @@ -25,7 +25,11 @@ export const EmailTemplates = () => { return (
{isError && ( - + )} {isLoading && (
diff --git a/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx b/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx index ac4fc3e093e9c..5a89a957b2468 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx @@ -10,6 +10,7 @@ import { useAuthHooksUpdateMutation } from 'data/auth/auth-hooks-update-mutation import { executeSql } from 'data/sql/execute-sql-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { cn } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { AddHookDropdown } from './AddHookDropdown' import { CreateHookSheet } from './CreateHookSheet' @@ -20,7 +21,12 @@ import { extractMethod, getRevokePermissionStatements, isValidHook } from './hoo export const HooksListing = () => { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() - const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) + const { + data: authConfig, + error: authConfigError, + isError, + isLoading, + } = useAuthConfigQuery({ projectRef }) const [selectedHook, setSelectedHook] = useState(null) const [selectedHookForDeletion, setSelectedHookForDeletion] = useState(null) @@ -60,10 +66,20 @@ export const HooksListing = () => { if (isError) { return ( - + + + + ) + } + + if (isLoading) { + return ( + + + ) } diff --git a/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx index b6bf44c39bc42..1f5cb248e5bc6 100644 --- a/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx @@ -7,6 +7,7 @@ import { boolean, number, object, string } from 'yup' import { useParams } from 'common' import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useAuthConfigQuery } from 'data/auth/auth-config-query' @@ -15,7 +16,6 @@ import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/constants' import { - AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, @@ -35,6 +35,7 @@ import { Switch, WarningIcon, } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -86,7 +87,12 @@ const securitySchema = object({ export const MfaAuthSettingsForm = () => { const { ref: projectRef } = useParams() - const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) + const { + data: authConfig, + error: authConfigError, + isError, + isLoading, + } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() // Separate loading states for each form @@ -253,16 +259,26 @@ export const MfaAuthSettingsForm = () => { if (isError) { return ( - - - Failed to retrieve auth configuration - {authConfigError.message} - + + + ) } if (!canReadConfig) { - return + return ( + + + + ) + } + + if (isLoading) { + return ( + + + + ) } const phoneMFAIsEnabled = diff --git a/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx index f5ef7d711ef1e..63f6674bf8b60 100644 --- a/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx @@ -9,6 +9,7 @@ import { boolean, number, object, string } from 'yup' import { useParams } from 'common' import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' import { InlineLink } from 'components/ui/InlineLink' import NoPermission from 'components/ui/NoPermission' import { useAuthConfigQuery } from 'data/auth/auth-config-query' @@ -16,9 +17,6 @@ import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutati import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { DOCS_URL } from 'lib/constants' import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, Badge, Button, Card, @@ -35,8 +33,8 @@ import { SelectValue_Shadcn_, Select_Shadcn_, Switch, - WarningIcon, } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { NO_REQUIRED_CHARACTERS } from '../Auth.constants' @@ -72,7 +70,12 @@ const schema = object({ export const ProtectionAuthSettingsForm = () => { const { ref: projectRef } = useParams() - const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) + const { + data: authConfig, + error: authConfigError, + isError, + isLoading, + } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation({ onError: (error) => { toast.error(`Failed to update settings: ${error?.message}`) @@ -145,16 +148,26 @@ export const ProtectionAuthSettingsForm = () => { if (isError) { return ( - - - Failed to retrieve auth configuration - {authConfigError.message} - + + + ) } if (!canReadConfig) { - return + return ( + + + + ) + } + + if (isLoading) { + return ( + + + + ) } return ( diff --git a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx index 82a5a1e14d920..55e086b2a44aa 100644 --- a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx +++ b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx @@ -142,15 +142,27 @@ export const RateLimits = () => { }, [isSuccess]) if (isError) { - return + return ( + + + + ) } if (!canReadConfig) { - return + return ( + + + + ) } if (isLoading) { - return + return ( + + + + ) } return ( diff --git a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx index 9e4f573f42113..76c31220454fa 100644 --- a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx +++ b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx @@ -7,21 +7,13 @@ import { ScaffoldSection, ScaffoldSectionTitle, } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import { HorizontalShimmerWithIcon } from 'components/ui/Shimmers/Shimmers' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { DOCS_URL } from 'lib/constants' -import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, - Button, - Modal, - ScrollArea, - WarningIcon, - cn, -} from 'ui' +import { Button, Modal, ScrollArea, cn } from 'ui' import { AddNewURLModal } from './AddNewURLModal' import { RedirectUrlList } from './RedirectUrlList' import { ValueContainer } from './ValueContainer' @@ -98,11 +90,7 @@ export const RedirectUrls = () => { )} {isError && ( - - - Failed to retrieve auth configuration - {authConfigError.message} - + )} {isSuccess && ( diff --git a/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx index 3cea989af221f..5e28e21bec217 100644 --- a/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx @@ -7,6 +7,7 @@ import * as z from 'zod' import { useParams } from 'common' import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useAuthConfigQuery } from 'data/auth/auth-config-query' @@ -15,9 +16,6 @@ import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/constants' import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, Button, Card, CardContent, @@ -28,8 +26,8 @@ import { Input_Shadcn_, PrePostTab, Switch, - WarningIcon, } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' function HoursOrNeverText({ value }: { value: number }) { @@ -58,7 +56,12 @@ const UserSessionsSchema = z.object({ export const SessionsAuthSettingsForm = () => { const { ref: projectRef } = useParams() - const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) + const { + data: authConfig, + error: authConfigError, + isError, + isLoading, + } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() // Separate loading states for each form @@ -155,16 +158,26 @@ export const SessionsAuthSettingsForm = () => { if (isError) { return ( - - - Failed to retrieve auth configuration - {authConfigError.message} - + + + ) } if (!canReadConfig) { - return + return ( + + + + ) + } + + if (isLoading) { + return ( + + + + ) } return ( diff --git a/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx b/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx index b1fe49a1f550e..5c38c88261d82 100644 --- a/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx +++ b/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx @@ -1,6 +1,5 @@ import { yupResolver } from '@hookform/resolvers/yup' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { AlertCircle } from 'lucide-react' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' @@ -8,13 +7,11 @@ import { object, string } from 'yup' import { useParams } from 'common' import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, Button, Card, CardContent, @@ -24,6 +21,7 @@ import { Form_Shadcn_, Input_Shadcn_, } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' const schema = object({ @@ -32,7 +30,12 @@ const schema = object({ const SiteUrl = () => { const { ref: projectRef } = useParams() - const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) + const { + data: authConfig, + error: authConfigError, + isError, + isLoading, + } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() const [isUpdatingSiteUrl, setIsUpdatingSiteUrl] = useState(false) @@ -76,11 +79,17 @@ const SiteUrl = () => { if (isError) { return ( - - - Failed to retrieve auth configuration - {authConfigError.message} - + + + + ) + } + + if (isLoading) { + return ( + + + ) } diff --git a/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx b/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx index 01cf200b2c40a..f8391f8339ce8 100644 --- a/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx +++ b/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx @@ -9,6 +9,7 @@ import * as yup from 'yup' import { useParams } from 'common' import { ScaffoldSection } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' @@ -27,7 +28,6 @@ import { Input_Shadcn_, PrePostTab, Switch, - WarningIcon, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { urlRegex } from '../Auth.constants' @@ -179,16 +179,18 @@ export const SmtpForm = () => { if (isError) { return ( - - - Failed to retrieve auth configuration - {authConfigError.message} - + + + ) } if (!canReadConfig) { - return + return ( + + + + ) } return ( diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index 74aab28e1c10c..2d3f39195b068 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -19,6 +19,7 @@ import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' import { useEdgeFunctionDeleteMutation } from 'data/edge-functions/edge-functions-delete-mutation' import { useEdgeFunctionUpdateMutation } from 'data/edge-functions/edge-functions-update-mutation' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { DOCS_URL } from 'lib/constants' import { Alert_Shadcn_, @@ -65,6 +66,14 @@ export const EdgeFunctionDetails = () => { '*' ) + const showAllEdgeFunctionInvocationExamples = useIsFeatureEnabled( + 'edge_functions:show_all_edge_function_invocation_examples' + ) + const invocationTabs = useMemo(() => { + if (showAllEdgeFunctionInvocationExamples) return INVOCATION_TABS + return INVOCATION_TABS.filter((tab) => tab.id === 'curl' || tab.id === 'supabase-js') + }, [showAllEdgeFunctionInvocationExamples]) + const { data: apiKeys } = useAPIKeysQuery({ projectRef }) const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) @@ -228,13 +237,13 @@ export const EdgeFunctionDetails = () => { - {INVOCATION_TABS.map((tab) => ( + {invocationTabs.map((tab) => ( {tab.label} ))} - {INVOCATION_TABS.map((tab) => ( + {invocationTabs.map((tab) => (
{ + const baseSchema = z.object({ organizationSlug: z.string().min(1, 'Please select an organization'), projectRef: z.string().min(1, 'Please select a project'), category: z.string().min(1, 'Please select an issue type'), @@ -90,15 +90,24 @@ const FormSchema = z affectedServices: z.string(), allowSupportAccess: z.boolean(), }) - .refine( - (data) => { - return !(data.category === 'Problem' && data.library === '') - }, - { - message: "Please select the library that you're facing issues with", - path: ['library'], - } - ) + + if (showClientLibraries) { + return baseSchema.refine( + (data) => { + return !(data.category === 'Problem' && data.library === '') + }, + { + message: "Please select the library that you're facing issues with", + path: ['library'], + } + ) + } + + // When showClientLibraries is false, make library optional and remove the refine validation + return baseSchema.extend({ + library: z.string().optional(), + }) +} const defaultValues = { organizationSlug: '', @@ -142,6 +151,7 @@ export const SupportFormV2 = ({ const dashboardSentryIssueId = router.query.sid as string const isBillingEnabled = useIsFeatureEnabled('billing:all') + const showClientLibraries = useIsFeatureEnabled('support:show_client_libraries') const categoryOptions = useMemo(() => { return CATEGORY_OPTIONS.filter((option) => { @@ -162,6 +172,8 @@ export const SupportFormV2 = ({ const [uploadedFiles, setUploadedFiles] = useState([]) const [uploadedDataUrls, setUploadedDataUrls] = useState([]) + const FormSchema = useMemo(() => createFormSchema(showClientLibraries), [showClientLibraries]) + const form = useForm>({ mode: 'onBlur', reValidateMode: 'onBlur', @@ -244,7 +256,9 @@ export const SupportFormV2 = ({ setIsSubmitting(true) const attachments = uploadedFiles.length > 0 ? await uploadAttachments(values.projectRef, uploadedFiles) : [] - const selectedLibrary = CLIENT_LIBRARIES.find((library) => library.language === values.library) + const selectedLibrary = values.library + ? CLIENT_LIBRARIES.find((library) => library.language === values.library) + : undefined const payload = { ...values, @@ -699,7 +713,7 @@ export const SupportFormV2 = ({ )}
- {category === 'Problem' && ( + {category === 'Problem' && showClientLibraries && ( )} - {library.length > 0 && } + {library && library.length > 0 && } {category !== 'Login_issues' && ( { - const { ref: projectRef } = useParams() - const { isLoading: isLoadingConfig } = useAuthConfigQuery({ projectRef }) const { can: canReadAuthSettings, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( PermissionAction.READ, 'custom_config_gotrue' @@ -28,10 +24,10 @@ const AuditLogsPage: NextPageWithLayout = () => { return ( - {!isPermissionsLoaded || isLoadingConfig ? ( -
+ {!isPermissionsLoaded ? ( + -
+ ) : ( )} diff --git a/packages/common/enabled-features/enabled-features.json b/packages/common/enabled-features/enabled-features.json index 678d1ef756086..3ecd75981cfaf 100644 --- a/packages/common/enabled-features/enabled-features.json +++ b/packages/common/enabled-features/enabled-features.json @@ -54,6 +54,7 @@ "docs:web_apps": true, "edge_functions:show_stripe_example": true, + "edge_functions:show_all_edge_function_invocation_examples": true, "feedback:docs": true, @@ -102,5 +103,7 @@ "sdk:python": true, "sdk:swift": true, - "search:fullIndex": true + "search:fullIndex": true, + + "support:show_client_libraries": true } diff --git a/packages/common/enabled-features/enabled-features.schema.json b/packages/common/enabled-features/enabled-features.schema.json index c0a812e7b504c..e678d317abcfe 100644 --- a/packages/common/enabled-features/enabled-features.schema.json +++ b/packages/common/enabled-features/enabled-features.schema.json @@ -197,6 +197,10 @@ "type": "boolean", "description": "Show all the Stripe example in edge function templates in the edge functions page." }, + "edge_functions:show_all_edge_function_invocation_examples": { + "type": "boolean", + "description": "Show all the invocation examples in the edge function details page. (If off, it will only show cURL and Javascript invocation examples)" + }, "feedback:docs": { "type": "boolean", @@ -350,6 +354,11 @@ "search:fullIndex": { "type": "boolean", "description": "Enable the full search index. When true, uses the full search; when false, uses the alternate search index." + }, + + "support:show_client_libraries": { + "type": "boolean", + "description": "Show the client libraries dropdown input and suggestions in the support form if the category 'API and client libraries' is selected" } }, "required": [