diff --git a/apps/docs/pages/guides/database/connecting-to-postgres.mdx b/apps/docs/pages/guides/database/connecting-to-postgres.mdx index c48c8bba747..10f810293f8 100644 --- a/apps/docs/pages/guides/database/connecting-to-postgres.mdx +++ b/apps/docs/pages/guides/database/connecting-to-postgres.mdx @@ -233,13 +233,13 @@ This is the most granular option. Connections are returned to the pool after eve - Navigate to the SSL tab and change the SSL mode to Require. Next navigate to the Root certificate input, it will open up a file-picker modal. Select the certificate you downloaded from your Supabase dashboard and save the server details. PgAdmin should now be able to connect to your Postgres via SSL. + Navigate to the Parameters tab and select connection parameter as Root Certificate. Next navigate to the Root certificate input, it will open up a file-picker modal. Select the certificate you downloaded from your Supabase dashboard and save the server details. PgAdmin should now be able to connect to your Postgres via SSL. - ![Add Connection Info.](/docs/img/guides/database/add-ssl-config.png) + ![Add Connection Info.](/docs/img/guides/database/add-ssl-config-parameter.png) diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-flutter.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-flutter.mdx index 2978ec82638..3a7de36b332 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-flutter.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-flutter.mdx @@ -34,49 +34,33 @@ Then let's install the only additional dependency: [`supabase_flutter`](https:// Copy and paste the following line in your pubspec.yaml to install the package: ```yaml -supabase_flutter: ^1.0.0 +supabase_flutter: ^1.10.3 ``` Run `flutter pub get` to install the dependencies. ### Setup deep links -Now that we have the dependencies installed let's setup deep links so users who have logged in via magic link or OAuth can come back to the app. +Now that we have the dependencies installed let's setup deep links. +Setting up deep links is required to bring back the user to the app when they click on the magic link to sign in. +We can setup deep links with just a minor tweak on our Flutter application. -Add `io.supabase.flutterquickstart://login-callback/` as a new [redirect URL](https://app.supabase.com/project/_/auth/url-configuration) in the Dashboard. +We will use `io.supabase.flutterquickstart` as the scheme, and `login-callback` as the host for our deep link in this example, but you can change it to whatever you would like. + +First, add `io.supabase.flutterquickstart://login-callback/` as a new [redirect URL](https://app.supabase.com/project/_/auth/url-configuration) in the Dashboard. ![Supabase console deep link setting](/docs/img/deeplink-setting.png) That is it on Supabase's end and the rest are platform specific settings: -For Android, edit the `android/app/src/main/AndroidManifest.xml` file. - -Add an intent-filter to enable deep linking: - -```xml android/app/src/main/AndroidManifest.xml - - - - - - - - - - - - - - - - - - -``` - -For iOS, edit the ios/Runner/Info.plist file. + + +Edit the `ios/Runner/Info.plist` file. Add CFBundleURLTypes to enable deep linking: @@ -103,9 +87,40 @@ Add CFBundleURLTypes to enable deep linking: ``` -For web: + + +Edit the `android/app/src/main/AndroidManifest.xml` file. +Add an intent-filter to enable deep linking: + +```xml android/app/src/main/AndroidManifest.xml + + + + + + + + + + + + + + + + + + +``` + + + There are no additional configurations. + + ### Main function @@ -115,44 +130,19 @@ These variables will be exposed on the app, and that's completely fine since we ```dart lib/main.dart Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - await Supabase.initialize( url: 'YOUR_SUPABASE_URL', anonKey: 'YOUR_SUPABASE_ANON_KEY', + authFlowType: AuthFlowType.pkce, ); runApp(MyApp()); } -``` - -### Setting up some constants and handy functions - -Let's also create a constant file to make it easier to use Supabase client. -We will also include an extension method declaration to call `showSnackBar` with one line of code. - -```dart lib/constants.dart -import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; final supabase = Supabase.instance.client; - -extension ShowSnackBar on BuildContext { - void showSnackBar({ - required String message, - Color backgroundColor = Colors.white, - }) { - ScaffoldMessenger.of(this).showSnackBar(SnackBar( - content: Text(message), - backgroundColor: backgroundColor, - )); - } - - void showErrorSnackBar({required String message}) { - showSnackBar(message: message, backgroundColor: Colors.red); - } -} ``` +`AuthFlowType.pkce` on `authFlowType` parameter indicates that we are using a secure [PKCE flow](https://supabase.com/blog/supabase-auth-sso-pkce#introducing-pkce) to perform our magic link login. + ### Set up Splash Screen Let's create a splash screen that will be shown to users right after they open the app. @@ -160,7 +150,7 @@ This screen retrieves the current session and redirects the user accordingly. ```dart lib/pages/splash_page.dart import 'package:flutter/material.dart'; -import 'package:supabase_quickstart/constants.dart'; +import 'package:supabase_quickstart/main.dart'; class SplashPage extends StatefulWidget { const SplashPage({super.key}); @@ -170,20 +160,18 @@ class SplashPage extends StatefulWidget { } class _SplashPageState extends State { - bool _redirectCalled = false; @override - void didChangeDependencies() { - super.didChangeDependencies(); + void initState() { + super.initState(); _redirect(); } Future _redirect() async { await Future.delayed(Duration.zero); - if (_redirectCalled || !mounted) { + if (!mounted) { return; } - _redirectCalled = true; final session = supabase.auth.currentSession; if (session != null) { Navigator.of(context).pushReplacementNamed('/account'); @@ -199,7 +187,6 @@ class _SplashPageState extends State { ); } } - ``` ### Set up a Login page @@ -215,8 +202,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; - -import 'package:supabase_quickstart/constants.dart'; +import 'package:supabase_quickstart/main.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -228,37 +214,46 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { bool _isLoading = false; bool _redirecting = false; - late final TextEditingController _emailController; + late final TextEditingController _emailController = TextEditingController(); late final StreamSubscription _authStateSubscription; Future _signIn() async { - setState(() { - _isLoading = true; - }); try { + setState(() { + _isLoading = true; + }); await supabase.auth.signInWithOtp( email: _emailController.text.trim(), emailRedirectTo: kIsWeb ? null : 'io.supabase.flutterquickstart://login-callback/', ); if (mounted) { - context.showSnackBar(message: 'Check your email for login link!'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Check your email for a login link!')), + ); _emailController.clear(); } } on AuthException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected error occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } } - - setState(() { - _isLoading = false; - }); } @override void initState() { - _emailController = TextEditingController(); _authStateSubscription = supabase.auth.onAuthStateChange.listen((data) { if (_redirecting) return; final session = data.session; @@ -310,8 +305,7 @@ Let's create a new widget called `account_page.dart` for that. ```dart lib/pages/account_page.dart" import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:supabase_quickstart/components/avatar.dart'; -import 'package:supabase_quickstart/constants.dart'; +import 'package:supabase_quickstart/main.dart'; class AccountPage extends StatefulWidget { const AccountPage({super.key}); @@ -323,8 +317,8 @@ class AccountPage extends StatefulWidget { class _AccountPageState extends State { final _usernameController = TextEditingController(); final _websiteController = TextEditingController(); - String? _avatarUrl; - var _loading = false; + + var _loading = true; /// Called once a user id is received within `onAuthenticated()` Future _getProfile() async { @@ -336,21 +330,28 @@ class _AccountPageState extends State { final userId = supabase.auth.currentUser!.id; final data = await supabase .from('profiles') - .select() + .select>() .eq('id', userId) - .single() as Map; + .single(); _usernameController.text = (data['username'] ?? '') as String; _websiteController.text = (data['website'] ?? '') as String; - _avatarUrl = (data['avatar_url'] ?? '') as String; } on PostgrestException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected exception occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + setState(() { + _loading = false; + }); + } } - - setState(() { - _loading = false; - }); } /// Called when user taps `Update` button @@ -370,28 +371,46 @@ class _AccountPageState extends State { try { await supabase.from('profiles').upsert(updates); if (mounted) { - context.showSnackBar(message: 'Successfully updated profile!'); + const SnackBar( + content: Text('Successfully updated profile!'), + ); } } on PostgrestException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpeted error occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + setState(() { + _loading = false; + }); + } } - setState(() { - _loading = false; - }); } Future _signOut() async { try { await supabase.auth.signOut(); } on AuthException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected error occurred'); - } - if (mounted) { - Navigator.of(context).pushReplacementNamed('/'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + Navigator.of(context).pushReplacementNamed('/login'); + } } } @@ -412,27 +431,29 @@ class _AccountPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Profile')), - body: ListView( - padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), - children: [ - TextFormField( - controller: _usernameController, - decoration: const InputDecoration(labelText: 'User Name'), - ), - const SizedBox(height: 18), - TextFormField( - controller: _websiteController, - decoration: const InputDecoration(labelText: 'Website'), - ), - const SizedBox(height: 18), - ElevatedButton( - onPressed: _updateProfile, - child: Text(_loading ? 'Saving...' : 'Update'), - ), - const SizedBox(height: 18), - TextButton(onPressed: _signOut, child: const Text('Sign Out')), - ], - ), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : ListView( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), + children: [ + TextFormField( + controller: _usernameController, + decoration: const InputDecoration(labelText: 'User Name'), + ), + const SizedBox(height: 18), + TextFormField( + controller: _websiteController, + decoration: const InputDecoration(labelText: 'Website'), + ), + const SizedBox(height: 18), + ElevatedButton( + onPressed: _loading ? null : _updateProfile, + child: Text(_loading ? 'Saving...' : 'Update'), + ), + const SizedBox(height: 18), + TextButton(onPressed: _signOut, child: const Text('Sign Out')), + ], + ), ); } } @@ -451,13 +472,14 @@ import 'package:supabase_quickstart/pages/splash_page.dart'; Future main() async { await Supabase.initialize( - // TODO: Replace credentials with your own url: 'YOUR_SUPABASE_URL', anonKey: 'YOUR_SUPABASE_ANON_KEY', ); runApp(MyApp()); } +final supabase = Supabase.instance.client; + class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { @@ -539,7 +561,7 @@ We can start by creating a new component: import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:supabase_quickstart/constants.dart'; +import 'package:supabase_quickstart/main.dart'; class Avatar extends StatefulWidget { const Avatar({ @@ -614,11 +636,21 @@ class _AvatarState extends State { widget.onUpload(imageUrlResponse); } on StorageException catch (error) { if (mounted) { - context.showErrorSnackBar(message: error.message); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); } } catch (error) { if (mounted) { - context.showErrorSnackBar(message: 'Unexpected error occurred'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); } } @@ -635,7 +667,7 @@ And then we can add the widget to the Account page as well as some logic to upda import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_quickstart/components/avatar.dart'; -import 'package:supabase_quickstart/constants.dart'; +import 'package:supabase_quickstart/main.dart'; class AccountPage extends StatefulWidget { const AccountPage({super.key}); @@ -647,8 +679,9 @@ class AccountPage extends StatefulWidget { class _AccountPageState extends State { final _usernameController = TextEditingController(); final _websiteController = TextEditingController(); + String? _avatarUrl; - var _loading = false; + var _loading = true; /// Called once a user id is received within `onAuthenticated()` Future _getProfile() async { @@ -660,21 +693,29 @@ class _AccountPageState extends State { final userId = supabase.auth.currentUser!.id; final data = await supabase .from('profiles') - .select() + .select>() .eq('id', userId) - .single() as Map; + .single(); _usernameController.text = (data['username'] ?? '') as String; _websiteController.text = (data['website'] ?? '') as String; _avatarUrl = (data['avatar_url'] ?? '') as String; } on PostgrestException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected exception occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + setState(() { + _loading = false; + }); + } } - - setState(() { - _loading = false; - }); } /// Called when user taps `Update` button @@ -694,28 +735,46 @@ class _AccountPageState extends State { try { await supabase.from('profiles').upsert(updates); if (mounted) { - context.showSnackBar(message: 'Successfully updated profile!'); + const SnackBar( + content: Text('Successfully updated profile!'), + ); } } on PostgrestException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpeted error occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + setState(() { + _loading = false; + }); + } } - setState(() { - _loading = false; - }); } Future _signOut() async { try { await supabase.auth.signOut(); } on AuthException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected error occurred'); - } - if (mounted) { - Navigator.of(context).pushReplacementNamed('/'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + Navigator.of(context).pushReplacementNamed('/login'); + } } } @@ -728,12 +787,20 @@ class _AccountPageState extends State { 'avatar_url': imageUrl, }); if (mounted) { - context.showSnackBar(message: 'Updated your profile image!'); + const SnackBar( + content: Text('Updated your profile image!'), + ); } } on PostgrestException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected error has occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); } if (!mounted) { return; @@ -761,32 +828,34 @@ class _AccountPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Profile')), - body: ListView( - padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), - children: [ - Avatar( - imageUrl: _avatarUrl, - onUpload: _onUpload, - ), - const SizedBox(height: 18), - TextFormField( - controller: _usernameController, - decoration: const InputDecoration(labelText: 'User Name'), - ), - const SizedBox(height: 18), - TextFormField( - controller: _websiteController, - decoration: const InputDecoration(labelText: 'Website'), - ), - const SizedBox(height: 18), - ElevatedButton( - onPressed: _updateProfile, - child: Text(_loading ? 'Saving...' : 'Update'), - ), - const SizedBox(height: 18), - TextButton(onPressed: _signOut, child: const Text('Sign Out')), - ], - ), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : ListView( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), + children: [ + Avatar( + imageUrl: _avatarUrl, + onUpload: _onUpload, + ), + const SizedBox(height: 18), + TextFormField( + controller: _usernameController, + decoration: const InputDecoration(labelText: 'User Name'), + ), + const SizedBox(height: 18), + TextFormField( + controller: _websiteController, + decoration: const InputDecoration(labelText: 'Website'), + ), + const SizedBox(height: 18), + ElevatedButton( + onPressed: _loading ? null : _updateProfile, + child: Text(_loading ? 'Saving...' : 'Update'), + ), + const SizedBox(height: 18), + TextButton(onPressed: _signOut, child: const Text('Sign Out')), + ], + ), ); } } diff --git a/apps/docs/public/img/guides/database/add-pg-server-conn-info.png b/apps/docs/public/img/guides/database/add-pg-server-conn-info.png index cbc868597fa..1198e74ee66 100644 Binary files a/apps/docs/public/img/guides/database/add-pg-server-conn-info.png and b/apps/docs/public/img/guides/database/add-pg-server-conn-info.png differ diff --git a/apps/docs/public/img/guides/database/add-ssl-config-parameter.png b/apps/docs/public/img/guides/database/add-ssl-config-parameter.png new file mode 100644 index 00000000000..dcd63aa8126 Binary files /dev/null and b/apps/docs/public/img/guides/database/add-ssl-config-parameter.png differ diff --git a/apps/docs/public/img/guides/database/add-ssl-config.png b/apps/docs/public/img/guides/database/add-ssl-config.png deleted file mode 100644 index e30d20ec590..00000000000 Binary files a/apps/docs/public/img/guides/database/add-ssl-config.png and /dev/null differ diff --git a/apps/docs/public/img/guides/database/name-pg-server.png b/apps/docs/public/img/guides/database/name-pg-server.png index 710d287ffe5..1f4f9af3506 100644 Binary files a/apps/docs/public/img/guides/database/name-pg-server.png and b/apps/docs/public/img/guides/database/name-pg-server.png differ diff --git a/apps/www/components/Pricing/PricingTableRow.tsx b/apps/www/components/Pricing/PricingTableRow.tsx index af3f8d6280c..ef443fb539b 100644 --- a/apps/www/components/Pricing/PricingTableRow.tsx +++ b/apps/www/components/Pricing/PricingTableRow.tsx @@ -89,7 +89,7 @@ export const PricingTableRowDesktop = (props: any) => { ) })} - + ) } @@ -144,7 +144,7 @@ export const PricingTableRowMobile = (props: any) => { })} - + ) } diff --git a/apps/www/data/CustomerStories.json b/apps/www/data/CustomerStories.json index c55ae5a858e..add15a48b42 100644 --- a/apps/www/data/CustomerStories.json +++ b/apps/www/data/CustomerStories.json @@ -1,4 +1,21 @@ [ + { + "type": "Customer Story", + "title": "Berri AI Boosts Productivity by Migrating from AWS RDS to Supabase with pgvector.", + "description": "Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase.", + "imgUrl": "images/customers/logos/berriai.png", + "logoUrl": "images/customers/logos/berriai.png", + "organization": "Berri AI", + "url": "/customers/berriai", + "path": "/customers/berriai", + "postMeta": { + "name": "Paul Copplestone", + "avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4", + "publishDate": "Jun 6, 2023", + "readLength": 6 + }, + "ctaText": "View story" + }, { "type": "Customer Story", "title": "Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.", diff --git a/apps/www/pages/customers/[slug].tsx b/apps/www/pages/customers/[slug].tsx index befe43bea0a..7be097ca5b9 100644 --- a/apps/www/pages/customers/[slug].tsx +++ b/apps/www/pages/customers/[slug].tsx @@ -5,13 +5,13 @@ import { MDXRemote } from 'next-mdx-remote' import { NextSeo } from 'next-seo' import Image from 'next/image' import Link from 'next/link' -import { useRouter } from 'next/router' -import { IconChevronLeft, IconChevronsLeft } from '~/../../packages/ui' +import { IconChevronLeft } from 'ui' import CTABanner from '~/components/CTABanner' import DefaultLayout from '~/components/Layouts/Default' import mdxComponents from '~/lib/mdx/mdxComponents' import { mdxSerialize } from '~/lib/mdx/mdxSerialize' import { getAllPostSlugs, getPostdata, getSortedPosts } from '~/lib/posts' +import { SITE_ORIGIN } from '~/lib/constants' // table of contents extractor const toc = require('markdown-toc') @@ -59,13 +59,14 @@ export async function getStaticProps({ params }: any) { function CaseStudyPage(props: any) { const content = props.blog.content - const { basePath } = useRouter() const meta = { title: props.blog.meta_title ?? `${props.blog.name} | Supabase Customer Stories`, description: props.blog.meta_description ?? props.blog.description, - image: props.blog.og_image ?? `${basePath}/images/customers/og/customer-stories.jpg`, - url: `https://supabase.io/customers/${props.blog.slug}`, + image: + `${SITE_ORIGIN}${props.blog.og_image}` ?? + `${SITE_ORIGIN}/images/customers/og/customer-stories.jpg`, + url: `${SITE_ORIGIN}/customers/${props.blog.slug}`, } return ( @@ -86,6 +87,7 @@ function CaseStudyPage(props: any) { images: [ { url: meta.image, + alt: `${meta.title} thumbnail`, }, ], }} diff --git a/examples/user-management/flutter-user-management/lib/main.dart b/examples/user-management/flutter-user-management/lib/main.dart index 656b7e22257..8b5a5969a24 100644 --- a/examples/user-management/flutter-user-management/lib/main.dart +++ b/examples/user-management/flutter-user-management/lib/main.dart @@ -9,6 +9,7 @@ Future main() async { // TODO: Replace credentials with your own url: 'YOUR_SUPABASE_URL', anonKey: 'YOUR_SUPABASE_ANON_KEY', + authFlowType: AuthFlowType.pkce, ); runApp(MyApp()); } diff --git a/examples/user-management/flutter-user-management/lib/pages/splash_page.dart b/examples/user-management/flutter-user-management/lib/pages/splash_page.dart index 82d3e80412e..126b46b7550 100644 --- a/examples/user-management/flutter-user-management/lib/pages/splash_page.dart +++ b/examples/user-management/flutter-user-management/lib/pages/splash_page.dart @@ -9,20 +9,18 @@ class SplashPage extends StatefulWidget { } class _SplashPageState extends State { - bool _redirectCalled = false; @override - void didChangeDependencies() { - super.didChangeDependencies(); + void initState() { + super.initState(); _redirect(); } Future _redirect() async { await Future.delayed(Duration.zero); - if (_redirectCalled || !mounted) { + if (!mounted) { return; } - _redirectCalled = true; final session = supabase.auth.currentSession; if (session != null) { Navigator.of(context).pushReplacementNamed('/account'); diff --git a/packages/shared-data/Pricing.json b/packages/shared-data/Pricing.json index b24b43f883f..50eb140d3ca 100644 --- a/packages/shared-data/Pricing.json +++ b/packages/shared-data/Pricing.json @@ -27,7 +27,7 @@ { "title": "Database size", "tooltips": { - "main": "Billing is based on the average database size in GB throughout the billing period." + "main": "Billing is based on the average daily database size in GB throughout the billing period." }, "plans": { "free": "500 MB", @@ -79,7 +79,7 @@ { "title": "Database egress", "tooltips": { - "main": "Billing is based on the total sum of outgoing traffic of your database in GB throughout your billing period. Only download operations are counted towards the limit" + "main": "Billing is based on the total sum of outgoing traffic of your database in GB throughout your billing period." }, "plans": { "free": "2GB", @@ -107,7 +107,7 @@ { "title": "MAUs", "tooltips": { - "main": "Distinct users requesting your API throughout the billing period." + "main": "Users who log in or refresh their token count towards MAU.\nBilling is based on the sum of distinct users requesting your API throughout the billing period. Resets every billing cycle." }, "plans": { "free": "50,000", @@ -199,7 +199,7 @@ { "title": "Storage", "tooltips": { - "main": "The sum of all objects' size in your storage buckets." + "main": "The sum of all objects' size in your storage buckets.\nBilling is based on the average daily size in GB throughout your billing period." }, "plans": { "free": "1 GB", @@ -211,7 +211,7 @@ { "title": "Storage egress", "tooltips": { - "main": "Total sum of outgoing traffic (egress) from your storage buckets, only download operations are counted." + "main": "All requests to view and download your storage items go through our CDN. We sum up all outgoing traffic (egress) for storage related requests through our CDN. We do not differentiate between cache and no cache hits.\nBilling is based on the total amount of egress in GB throughout your billing period." }, "plans": { "free": "2 GB", @@ -242,7 +242,7 @@ { "title": "Image Transformations", "tooltips": { - "main": "Distinct count of all images that were transformed in the billing period, ignoring any transformations." + "main": "We count all images that were transformed in the billing period, ignoring any transformations.\nUsage example: You transform one image with four different size transformations and another image with just a single transformation. It counts as two, as only two images were transformed.\nBilling is based on the count of (origin) images that used transformations throughout the billing period. Resets every billing cycle." }, "plans": { "free": false, @@ -309,7 +309,7 @@ { "title": "Concurrent Peak Connections", "tooltips": { - "main": "We only charge for successful connections - not connection attempts!" + "main": "Total number of successful connections. Connections attempts are not counted towards usage.\nBilling is based on the maximum amount of concurrent peak connections throughout your billing period." }, "plans": { "free": "200", @@ -321,7 +321,7 @@ { "title": "Messages Per Month", "tooltips": { - "main": "Billing is based on the total sum of Realtime messages throughout your billing period." + "main": "Count of messages going through Realtime.\nUsage example: If you do a database change and 5 clients listen to that change via Realtime, that's 5 messages. If you broadcast a message and 4 clients listen to that, that's 5 messages (1 message sent, 4 received).\nBilling is based on the total amount of messages throughout your billing period." }, "plans": { "free": "2 Million", diff --git a/studio/components/interfaces/Billing/EnterpriseUpdate.tsx b/studio/components/interfaces/Billing/EnterpriseUpdate.tsx index 0275f33ae61..b94d583c065 100644 --- a/studio/components/interfaces/Billing/EnterpriseUpdate.tsx +++ b/studio/components/interfaces/Billing/EnterpriseUpdate.tsx @@ -27,6 +27,8 @@ import { import { PaymentMethod, SubscriptionPreview } from './Billing.types' import { formSubscriptionUpdatePayload, getCurrentAddons } from './Billing.utils' import SupportPlan from './AddOns/SupportPlan' +import { subscriptionKeys } from 'data/subscriptions/keys' +import { useQueryClient } from '@tanstack/react-query' interface Props { products: { tiers: any[]; addons: SubscriptionAddon[] } @@ -45,6 +47,7 @@ const EnterpriseUpdate: FC = ({ }) => { const { app, ui } = useStore() const router = useRouter() + const queryClient = useQueryClient() const [captchaToken, setCaptchaToken] = useState(null) const captchaRef = useRef(null) @@ -195,6 +198,12 @@ const EnterpriseUpdate: FC = ({ } else { setIsSuccessful(true) } + + await Promise.all([ + queryClient.invalidateQueries(subscriptionKeys.subscriptionV2(projectRef)), + queryClient.invalidateQueries(subscriptionKeys.subscription(projectRef)), + queryClient.invalidateQueries(subscriptionKeys.addons(projectRef)), + ]) } setIsSubmitting(false) } diff --git a/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx b/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx index b488e3011b7..365e59dbd64 100644 --- a/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx +++ b/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx @@ -13,6 +13,8 @@ import { SubscriptionPreview } from '../Billing.types' import { StripeSubscription } from 'components/interfaces/Billing' import HCaptcha from '@hcaptcha/react-hcaptcha' import { CANCELLATION_REASONS } from 'components/interfaces/BillingV2/Billing.constants' +import { subscriptionKeys } from 'data/subscriptions/keys' +import { useQueryClient } from '@tanstack/react-query' interface ExitSurveyProps { freeTier: any @@ -23,6 +25,8 @@ interface ExitSurveyProps { const ExitSurvey = ({ freeTier, subscription, onSelectBack }: ExitSurveyProps) => { const router = useRouter() const { app, ui } = useStore() + const queryClient = useQueryClient() + const projectId = ui.selectedProject?.id ?? -1 const projectRef = ui.selectedProject?.ref @@ -165,6 +169,12 @@ const ExitSurvey = ({ freeTier, subscription, onSelectBack }: ExitSurveyProps) = } else { setIsSuccessful(true) } + + await Promise.all([ + queryClient.invalidateQueries(subscriptionKeys.subscriptionV2(projectRef)), + queryClient.invalidateQueries(subscriptionKeys.subscription(projectRef)), + queryClient.invalidateQueries(subscriptionKeys.addons(projectRef)), + ]) } const feedbackRes = await post(`${API_URL}/feedback/downgrade`, { projectRef, diff --git a/studio/components/interfaces/Billing/ProUpgrade.tsx b/studio/components/interfaces/Billing/ProUpgrade.tsx index 6ebc73b9181..044fc420df8 100644 --- a/studio/components/interfaces/Billing/ProUpgrade.tsx +++ b/studio/components/interfaces/Billing/ProUpgrade.tsx @@ -30,6 +30,8 @@ import BackButton from 'components/ui/BackButton' import SupportPlan from './AddOns/SupportPlan' import HCaptcha from '@hcaptcha/react-hcaptcha' import SpendCapModal from './SpendCapModal' +import { useQueryClient } from '@tanstack/react-query' +import { subscriptionKeys } from 'data/subscriptions/keys' // Do not allow compute size changes for af-south-1 @@ -52,6 +54,7 @@ const ProUpgrade: FC = ({ }) => { const { app, ui } = useStore() const router = useRouter() + const queryClient = useQueryClient() const [captchaToken, setCaptchaToken] = useState(null) const captchaRef = useRef(null) @@ -212,6 +215,12 @@ const ProUpgrade: FC = ({ } else { setIsSuccessful(true) } + + await Promise.all([ + queryClient.invalidateQueries(subscriptionKeys.subscriptionV2(projectRef)), + queryClient.invalidateQueries(subscriptionKeys.subscription(projectRef)), + queryClient.invalidateQueries(subscriptionKeys.addons(projectRef)), + ]) } setIsSubmitting(false) } diff --git a/studio/components/interfaces/Billing/TeamUpgrade.tsx b/studio/components/interfaces/Billing/TeamUpgrade.tsx index f0e83f6bb5f..48b2dde8dcb 100644 --- a/studio/components/interfaces/Billing/TeamUpgrade.tsx +++ b/studio/components/interfaces/Billing/TeamUpgrade.tsx @@ -27,6 +27,8 @@ import { } from './AddOns/AddOns.utils' import SupportPlan from './AddOns/SupportPlan' import HCaptcha from '@hcaptcha/react-hcaptcha' +import { subscriptionKeys } from 'data/subscriptions/keys' +import { useQueryClient } from '@tanstack/react-query' // Do not allow compute size changes for af-south-1 @@ -49,10 +51,12 @@ const TeamUpgrade = ({ const router = useRouter() // Team tier is enabled when the user is already on the team tier (manually assigned by us) - const teamTierEnabled = currentSubscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM + const teamTierEnabled = + currentSubscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM const [captchaToken, setCaptchaToken] = useState(null) const captchaRef = useRef(null) + const queryClient = useQueryClient() const { addons } = products const computeSizes = formatComputeSizes(addons) @@ -202,6 +206,12 @@ const TeamUpgrade = ({ } else { setIsSuccessful(true) } + + await Promise.all([ + queryClient.invalidateQueries(subscriptionKeys.subscriptionV2(projectRef)), + queryClient.invalidateQueries(subscriptionKeys.subscription(projectRef)), + queryClient.invalidateQueries(subscriptionKeys.addons(projectRef)), + ]) } setIsSubmitting(false) } diff --git a/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx b/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx index e84529e4ef2..d55aaf1ca1f 100644 --- a/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx +++ b/studio/components/interfaces/BillingV2/Usage/Usage.constants.tsx @@ -156,7 +156,6 @@ export const USAGE_CATEGORIES: CategoryMeta[] = [ const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD const isExceededLimit = hasLimit && usageRatio >= 1 - console.log({ usageMeta, isApproachingLimit, isExceededLimit }) return subscription?.plan?.id !== 'free' ? (
@@ -177,10 +176,10 @@ export const USAGE_CATEGORIES: CategoryMeta[] = [

- Your disk storage expands automatically when the database reaches 90% of the disk - size. The disk is expanded to be 50% larger. Auto-scaling can only take place once - every 6 hours. If you reach 95% of the disk space, your project will enter read-only - mode. You can also{' '} + If you reach 95% of the disk space, your project will enter read-only mode. Your + disk storage expands automatically when the database reaches 90% of the disk size. + The disk is expanded to be 50% larger. Auto-scaling can only take place once every 6 + hours. You can also{' '} preprovision {' '} diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts index feb16cbd553..610ceec29e1 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts @@ -553,7 +553,7 @@ export const WRAPPERS: WrapperMeta[] = [ name: 's3_wrapper', handlerName: 's3_fdw_handler', validatorName: 's3_fdw_validator', - icon: '/img/icons/s3-icon.svg', + icon: `${BASE_PATH}/img/icons/s3-icon.svg`, extensionName: 'S3Fdw', label: 'S3', docsUrl: 'https://supabase.github.io/wrappers/s3/', @@ -636,7 +636,7 @@ export const WRAPPERS: WrapperMeta[] = [ name: 'clickhouse_wrapper', handlerName: 'click_house_fdw_handler', validatorName: 'click_house_fdw_validator', - icon: '/img/icons/clickhouse-icon.svg', + icon: `${BASE_PATH}/img/icons/clickhouse-icon.svg`, extensionName: 'ClickHouseFdw', label: 'ClickHouse', docsUrl: 'https://supabase.github.io/wrappers/clickhouse/', @@ -676,4 +676,78 @@ export const WRAPPERS: WrapperMeta[] = [ }, ], }, + { + name: 'bigquery_wrapper', + handlerName: 'big_query_fdw_handler', + validatorName: 'big_query_fdw_validator', + icon: `${BASE_PATH}/img/icons/bigquery-icon.svg`, + extensionName: 'BigQueryFdw', + label: 'BigQuery', + docsUrl: 'https://supabase.github.io/wrappers/bigquery/', + server: { + options: [ + { + name: 'sa_key_id', + label: 'Service Account Key', + required: true, + encrypted: true, + hidden: true, + }, + { + name: 'project_id', + label: 'Project ID', + required: true, + encrypted: false, + hidden: false, + }, + { + name: 'dataset_id', + label: 'Dataset ID', + required: true, + encrypted: false, + hidden: false, + }, + ], + }, + tables: [ + { + label: 'BigQuery Table', + description: 'Map to a BigQuery Table', + options: [ + { + name: 'table', + label: 'BigQuery Table Name', + editable: true, + required: true, + placeholder: 'my_bigquery_table', + type: 'text', + }, + { + name: 'location', + label: 'Location', + defaultValue: 'US', + editable: true, + required: false, + type: 'text', + }, + { + name: 'timeout', + label: 'Timeout (ms)', + defaultValue: '30000', + editable: true, + required: false, + type: 'text', + }, + { + name: 'rowid_column', + label: 'Row ID Column', + defaultValue: 'id', + editable: true, + required: false, + type: 'text', + }, + ], + }, + ], + }, ] diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx index 7e18fc5a46e..f02483fb224 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx @@ -45,7 +45,7 @@ const Wrappers = () => {

) : (