Merge branch 'master' into feat/button-shadcn

This commit is contained in:
Jonathan Summers-Muir committed 2023-06-14 01:27:41 +08:00
commit caaef2a46f
22 files changed
+412 -214

No files matched your search

@@ -233,13 +233,13 @@ This is the most granular option. Connections are returned to the pool after eve
<StepHikeCompact.Details title="SSL">
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.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
![Add Connection Info.](/docs/img/guides/database/add-ssl-config.png)
![Add Connection Info.](/docs/img/guides/database/add-ssl-config-parameter.png)
</StepHikeCompact.Code>
@@ -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
<manifest ...>
<!-- ... other tags -->
<application ...>
<activity ...>
<!-- ... other tags -->
<!-- Add this intent-filter for Deep Links -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
<data
android:scheme="io.supabase.flutterquickstart"
android:host="login-callback" />
</intent-filter>
</activity>
</application>
</manifest>
```
For iOS, edit the ios/Runner/Info.plist file.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="ios"
>
<TabPanel id="ios" label="iOS">
Edit the `ios/Runner/Info.plist` file.
Add CFBundleURLTypes to enable deep linking:
@@ -103,9 +87,40 @@ Add CFBundleURLTypes to enable deep linking:
</plist>
```
For web:
</TabPanel>
<TabPanel id="android" label="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
<manifest ...>
<!-- ... other tags -->
<application ...>
<activity ...>
<!-- ... other tags -->
<!-- Add this intent-filter for Deep Links -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
<data
android:scheme="io.supabase.flutterquickstart"
android:host="login-callback" />
</intent-filter>
</activity>
</application>
</manifest>
```
</TabPanel>
<TabPanel id="web" label="Web">
There are no additional configurations.
</TabPanel>
</Tabs>
### 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<void> 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<SplashPage> {
bool _redirectCalled = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
void initState() {
super.initState();
_redirect();
}
Future<void> _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<SplashPage> {
);
}
}
```
### 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<LoginPage> {
bool _isLoading = false;
bool _redirecting = false;
late final TextEditingController _emailController;
late final TextEditingController _emailController = TextEditingController();
late final StreamSubscription<AuthState> _authStateSubscription;
Future<void> _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<AccountPage> {
final _usernameController = TextEditingController();
final _websiteController = TextEditingController();
String? _avatarUrl;
var _loading = false;
var _loading = true;
/// Called once a user id is received within `onAuthenticated()`
Future<void> _getProfile() async {
@@ -336,21 +330,28 @@ class _AccountPageState extends State<AccountPage> {
final userId = supabase.auth.currentUser!.id;
final data = await supabase
.from('profiles')
.select()
.select<Map<String, dynamic>>()
.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<AccountPage> {
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<void> _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<AccountPage> {
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<void> 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<Avatar> {
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<AccountPage> {
final _usernameController = TextEditingController();
final _websiteController = TextEditingController();
String? _avatarUrl;
var _loading = false;
var _loading = true;
/// Called once a user id is received within `onAuthenticated()`
Future<void> _getProfile() async {
@@ -660,21 +693,29 @@ class _AccountPageState extends State<AccountPage> {
final userId = supabase.auth.currentUser!.id;
final data = await supabase
.from('profiles')
.select()
.select<Map<String, dynamic>>()
.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<AccountPage> {
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<void> _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<AccountPage> {
'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<AccountPage> {
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')),
],
),
);
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

After

Width:  |  Height:  |  Size: 23 KiB

@@ -89,7 +89,7 @@ export const PricingTableRowDesktop = (props: any) => {
</Fragment>
)
})}
<ReactTooltip effect={'solid'} className="!max-w-[320px]" />
<ReactTooltip effect={'solid'} className="!max-w-[320px] whitespace-pre-line" />
</>
)
}
@@ -144,7 +144,7 @@ export const PricingTableRowMobile = (props: any) => {
})}
</tbody>
</table>
<ReactTooltip effect={'solid'} className="!max-w-[320px]" />
<ReactTooltip effect={'solid'} className="!max-w-[320px] whitespace-pre-line" />
</>
)
}
+17
View File
@@ -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.",
+7 -5
View File
@@ -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`,
},
],
}}
@@ -9,6 +9,7 @@ Future<void> main() async {
// TODO: Replace credentials with your own
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_SUPABASE_ANON_KEY',
authFlowType: AuthFlowType.pkce,
);
runApp(MyApp());
}
@@ -9,20 +9,18 @@ class SplashPage extends StatefulWidget {
}
class _SplashPageState extends State<SplashPage> {
bool _redirectCalled = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
void initState() {
super.initState();
_redirect();
}
Future<void> _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');
+8 -8
View File
@@ -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",
@@ -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<Props> = ({
}) => {
const { app, ui } = useStore()
const router = useRouter()
const queryClient = useQueryClient()
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
const captchaRef = useRef<HCaptcha>(null)
@@ -195,6 +198,12 @@ const EnterpriseUpdate: FC<Props> = ({
} else {
setIsSuccessful(true)
}
await Promise.all([
queryClient.invalidateQueries(subscriptionKeys.subscriptionV2(projectRef)),
queryClient.invalidateQueries(subscriptionKeys.subscription(projectRef)),
queryClient.invalidateQueries(subscriptionKeys.addons(projectRef)),
])
}
setIsSubmitting(false)
}
@@ -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,
@@ -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<Props> = ({
}) => {
const { app, ui } = useStore()
const router = useRouter()
const queryClient = useQueryClient()
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
const captchaRef = useRef<HCaptcha>(null)
@@ -212,6 +215,12 @@ const ProUpgrade: FC<Props> = ({
} else {
setIsSuccessful(true)
}
await Promise.all([
queryClient.invalidateQueries(subscriptionKeys.subscriptionV2(projectRef)),
queryClient.invalidateQueries(subscriptionKeys.subscription(projectRef)),
queryClient.invalidateQueries(subscriptionKeys.addons(projectRef)),
])
}
setIsSubmitting(false)
}
@@ -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<string | null>(null)
const captchaRef = useRef<HCaptcha>(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)
}
@@ -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' ? (
<div>
@@ -177,10 +176,10 @@ export const USAGE_CATEGORIES: CategoryMeta[] = [
</Link>
</div>
<p className="text-sm text-scale-1000 mt-3">
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{' '}
<Link passHref href={`/project/${ref}/settings/database#diskManagement`}>
<a className="text-brand-900 transition hover:text-brand-1000">preprovision</a>
</Link>{' '}
@@ -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',
},
],
},
],
},
]
@@ -45,7 +45,7 @@ const Wrappers = () => {
</div>
</div>
<div className="flex items-center space-x-2">
<Link href="">
<Link href="https://supabase.com/docs/guides/database/extensions/wrappers">
<a target="_blank" rel="noreferrer">
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
Documentation
@@ -104,7 +104,7 @@ const WrappersDisabledState = () => {
</div>
</div>
<div className="flex items-center space-x-2 my-1 ml-[1px]">
<Link href="https://supabase.com/docs/guides/database/wrappers">
<Link href="https://supabase.com/docs/guides/database/extensions/wrappers">
<a target="_blank" rel="noreferrer">
<Button type="default" icon={<IconExternalLink />}>
About Wrappers
@@ -115,7 +115,7 @@ const WrappersDisabledState = () => {
</div>
) : (
<div className="flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/database/wrappers">
<Link href="https://supabase.com/docs/guides/database/extensions/wrappers">
<a target="_blank" rel="noreferrer">
<Button type="default" icon={<IconExternalLink />}>
About Wrappers
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path fill="#4386fa" d="M14.48 58.196.558 34.082a4.163 4.163 0 0 1 0-4.164L14.48 5.805a4.163 4.163 0 0 1 3.6-2.082h27.857c1.48.007 2.845.8 3.585 2.082l13.92 24.113a4.163 4.163 0 0 1 0 4.164L49.52 58.196a4.163 4.163 0 0 1-3.6 2.082H18.07a4.167 4.167 0 0 1-3.593-2.082z"/><path d="M40.697 24.235s3.87 9.283-1.406 14.545-14.883 1.894-14.883 1.894L43.95 60.27h1.984a4.163 4.163 0 0 0 3.6-2.082L58.75 42.23z" opacity=".1"/><path fill="#fff" d="M45.267 43.23 41 38.953a.67.67 0 0 0-.158-.12 11.63 11.63 0 1 0-2.032 2.037.67.67 0 0 0 .113.15l4.277 4.277a.67.67 0 0 0 .947 0l1.12-1.12a.67.67 0 0 0 0-.947zM31.64 40.464a8.75 8.75 0 1 1 8.749-8.749 8.75 8.75 0 0 1-8.749 8.749zm-5.593-9.216v3.616a6.426 6.426 0 0 0 2.338 2.375v-6.013zm4.375-2.998v9.772a6.45 6.45 0 0 0 2.338 0V28.25zm6.764 6.606v-2.142H34.85v4.5a6.43 6.43 0 0 0 2.338-2.368z"/></svg>

After

Width:  |  Height:  |  Size: 903 B

-1
View File
@@ -407,7 +407,6 @@ export default class MetaStore implements IMetaStore {
try {
// Once pg-meta supports composite keys, we can remove this logic
const { isPrimaryKey, ...formattedPayload } = payload
const column: any = await this.columns.create(formattedPayload)
if (column.error) throw column.error