From 7a7bd7b78c9240f574e75231e4445bc7d3b234bc Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Thu, 13 Jul 2023 19:41:01 +0200 Subject: [PATCH 01/80] Update targets --- apps/docs/docs/ref/kotlin/introduction.mdx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/docs/docs/ref/kotlin/introduction.mdx b/apps/docs/docs/ref/kotlin/introduction.mdx index 7f1f3784175..6dd0279006a 100644 --- a/apps/docs/docs/ref/kotlin/introduction.mdx +++ b/apps/docs/docs/ref/kotlin/introduction.mdx @@ -18,15 +18,17 @@ This reference documents every object and method available in Supabase's Kotlin Supported Kotlin targets: -| **Module** | **GoTrue** | **Realtime** | **Postgrest** | **Storage** | **Functions** | **Apollo-GraphQL** | -| -------------------------------------- | ---------- | ------------ | ------------- | ----------- | ------------- | ------------------ | -| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Android** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **JS** _(Browser)_ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **IOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **MacOS** _(macosX64 & macosArm64)_ 🚧 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Windows** _(mingwX64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | -| **Linux** _(linuxX64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | +| | **GoTrue** | **Realtime** | **Postgrest** | **Storage** | **Functions** | **Apollo-GraphQL** | +|--------------------------------------------------------------------|------------|--------------|---------------|-------------|---------------|--------------------| +| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Android** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **JS** *(Browser, NodeJS)* | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **IOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **tvOS** *(tvosArm64, tvosX64, tvosSimulatorArm64)* 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **watchOS** *(watchosArm64, watchosX64, watchosSimulatorArm64)* 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **MacOS** *(macosX64 & macosArm64)* 🚧 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Windows** *(mingwX64)* 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | +| **Linux** *(linuxX64)* 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ = full support From bf7a556a40eb464b23c395b1599723bc46d85194 Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Thu, 13 Jul 2023 20:08:34 +0200 Subject: [PATCH 02/80] Update docs --- spec/supabase_kt_v0.yml | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/spec/supabase_kt_v0.yml b/spec/supabase_kt_v0.yml index e3e0b032dd4..c40b82a89e0 100644 --- a/spec/supabase_kt_v0.yml +++ b/spec/supabase_kt_v0.yml @@ -33,7 +33,7 @@ functions: *Note: OTP link verification such as sign ups are not supported on JVM. You may have to send a verification token rather than a url in your E-Mail. To send the token, rather than a redirect url, you have to change `{{ .ConfirmationURL }}` in your sign up email to `{{ .Token }}`* - **On Android, IOS & MacOS**, it uses deeplinks. Refer to the guide below on how to setup deeplinks. + **On Android, IOS & MacOS**, it uses deeplinks. Refer to the guide below on how to setup deeplinks. Alternatively you could use Native Google Auth or a WebView for OAuth. Refer to our [demo](https://github.com/supabase-community/supabase-kt/tree/master/demos/android-login) to learn more. **On JS**, it uses the website origin as the callback url. Session importing gets handled automatically. **Windows & Linux** currently have no default implementation. Feel free to create a PR. @@ -596,7 +596,7 @@ functions: `delete()`, and `rpc()` code: | ```kotlin - client.postgrest["cities"].select(columns = Columns.list("name, country_id")) { + client.postgrest["cities"].select(columns = Columns.list("name", "country_id")) { City::name eq "The Shire" //or eq("name", "The Shire") @@ -2172,15 +2172,14 @@ functions: ```kotlin client.gotrue.loginWith(IDToken) { idToken = "token" - clientId = "oauth client id" - provider = Google //Or Apple + provider = Google //Also supported: Apple, Azure and Facebook //optional: nonce = "nonce" data = buildJsonObject { //... } } - ``` + - id: sign-in-with-otp title: 'sendOtpTo()' $ref: '@supabase/gotrue-js.GoTrueClient.signInWithOtp' @@ -2313,9 +2312,23 @@ functions: name: Sign out isSpotlight: true code: | - ```js + ```kotlin client.gotrue.logout() ``` + - id: sign-out-all-sessions + name: Sign out all sessions + isSpotlight: true + code: | + ```kotlin + client.gotrue.logout(LogoutScope.GLOBAL) + ``` + - id: sign-out-all-sessions + name: Sign out all sessions except yours + isSpotlight: true + code: | + ```kotlin + client.gotrue.logout(LogoutScope.OTHERS) + ``` - id: verify-otp title: 'Verify OTPs' $ref: '@supabase/gotrue-js.GoTrueClient.verifyOtp' @@ -3114,6 +3127,7 @@ functions: println(it) } + client.realtime.connect() channel.join(blockUntilJoined = true) channel.broadcast(event = "message", Message("I joined!", "John")) @@ -3138,6 +3152,7 @@ functions: connectedUsers -= it.decodeLeavesAs() } + client.realtime.connect() channel.join(blockUntilJoined = true) //send own state channel.track(PresenceState(username = "John")) @@ -3160,6 +3175,7 @@ functions: } } + client.realtime.connect() channel.join() ``` - id: listen-to-a-specific-table @@ -3183,6 +3199,7 @@ functions: } } + client.realtime.connect() channel.join() ``` - id: listen-to-inserts @@ -3201,6 +3218,7 @@ functions: println(it.record) } + client.realtime.connect() channel.join() ``` - id: listen-to-updates @@ -3227,6 +3245,7 @@ functions: println(it.oldRecord) } + client.realtime.connect() channel.join() ``` - id: listen-to-deletes @@ -3252,6 +3271,7 @@ functions: println(it.oldRecord) } + client.realtime.connect() channel.join() ``` - id: listening-to-row-level-changes @@ -3272,6 +3292,7 @@ functions: println(it.oldRecord) } + client.realtime.connect() channel.join() ``` From b86ee27683cdab985432b3a437a290cb0bf99127 Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Thu, 13 Jul 2023 20:11:34 +0200 Subject: [PATCH 03/80] Update docs --- spec/supabase_kt_v0.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/supabase_kt_v0.yml b/spec/supabase_kt_v0.yml index c40b82a89e0..40f2c4ee1c2 100644 --- a/spec/supabase_kt_v0.yml +++ b/spec/supabase_kt_v0.yml @@ -35,7 +35,7 @@ functions: **On Android, IOS & MacOS**, it uses deeplinks. Refer to the guide below on how to setup deeplinks. Alternatively you could use Native Google Auth or a WebView for OAuth. Refer to our [demo](https://github.com/supabase-community/supabase-kt/tree/master/demos/android-login) to learn more. **On JS**, it uses the website origin as the callback url. Session importing gets handled automatically. - **Windows & Linux** currently have no default implementation. Feel free to create a PR. + **Windows, tvOS, watchOS & Linux** currently have no default implementation. Feel free to create a PR. You always make your own implementation and use `gotrue.parseFragmentAndImportSession(fragment)` to let [supabase-kt](https://github.com/supabase-community/supabase-kt) handle the parsing & importing of the session after receiving a callback. @@ -2179,6 +2179,7 @@ functions: //... } } + ``` - id: sign-in-with-otp title: 'sendOtpTo()' From 26ae36220845793f94b9dc74f5b5ed7c6a61eb37 Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Thu, 13 Jul 2023 20:13:03 +0200 Subject: [PATCH 04/80] Fix logout docs --- spec/supabase_kt_v0.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/supabase_kt_v0.yml b/spec/supabase_kt_v0.yml index 40f2c4ee1c2..0a0320cebcf 100644 --- a/spec/supabase_kt_v0.yml +++ b/spec/supabase_kt_v0.yml @@ -2323,7 +2323,7 @@ functions: ```kotlin client.gotrue.logout(LogoutScope.GLOBAL) ``` - - id: sign-out-all-sessions + - id: sign-out-others name: Sign out all sessions except yours isSpotlight: true code: | From fc1b3aaf626b411b7f818085a7fc9762038b6fcc Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Thu, 13 Jul 2023 20:13:52 +0200 Subject: [PATCH 05/80] update doc wording --- spec/supabase_kt_v0.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/supabase_kt_v0.yml b/spec/supabase_kt_v0.yml index 0a0320cebcf..cb18c10013f 100644 --- a/spec/supabase_kt_v0.yml +++ b/spec/supabase_kt_v0.yml @@ -2324,7 +2324,7 @@ functions: client.gotrue.logout(LogoutScope.GLOBAL) ``` - id: sign-out-others - name: Sign out all sessions except yours + name: Sign out all sessions except the current isSpotlight: true code: | ```kotlin From 1e16460244cb26c8c9875b40eb259163f42e7474 Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Mon, 17 Jul 2023 17:16:08 +0200 Subject: [PATCH 06/80] Improve docs --- spec/supabase_kt_v0.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/spec/supabase_kt_v0.yml b/spec/supabase_kt_v0.yml index cb18c10013f..3be385b3168 100644 --- a/spec/supabase_kt_v0.yml +++ b/spec/supabase_kt_v0.yml @@ -45,18 +45,26 @@ functions: 1. **Setup a deeplink for you app** \ On Android you may setup a [deeplink](https://developer.android.com/training/app-links/deep-linking) in your Android manifest. \ On IOS & MacOS you may setup a [url scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). - 2. **Configure the GoTrue plugin** + 2. **Add your deeplink to the [redirect URLs](https://supabase.com/dashboard/project/_/auth/url-configuration)** \ + **Pattern**: scheme://host + 3. **Configure the GoTrue plugin** You have to set the `host` and the `scheme` in the GoTrue config: ```kotlin install(GoTrue) { - host = "deeplink host" + host = "deeplink host" //that can be anything for example your package name, app/company url. (Not your supabase url) scheme = "deeplink scheme" } ``` - 3. **Call platform specific function on startup** \ + 4. **Call platform specific function on startup** \ On Android: `client.handleDeeplinks(intent)` \ On IOS/MacOS: `client.handleDeeplinks(url)` + Then you can just login using OAuth: + ```kotlin + client.gotrue.loginWith(Google) + ``` + Or open OTP links directly in your app. + ### PKCE Authentication flow suapbase-kt supports the [PKCE authentication flow](https://supabase.com/blog/supabase-auth-sso-pkce). To use it, you just have to change the `flowType` in the GoTrue configuration: @@ -65,6 +73,7 @@ functions: flowType = FlowType.PKCE } ``` + That's it! If you already implemented deeplinks to handle OTPs and OAuth you don't have to change anything! examples: - id: initialize-client name: Initialize Client From e51321c73ff7c4370bd19ba392f82cbabc4b1483 Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Mon, 17 Jul 2023 17:19:46 +0000 Subject: [PATCH 07/80] Update spec/supabase_kt_v0.yml Co-authored-by: Greg Richardson --- spec/supabase_kt_v0.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/supabase_kt_v0.yml b/spec/supabase_kt_v0.yml index 3be385b3168..20ded9e5963 100644 --- a/spec/supabase_kt_v0.yml +++ b/spec/supabase_kt_v0.yml @@ -51,7 +51,7 @@ functions: You have to set the `host` and the `scheme` in the GoTrue config: ```kotlin install(GoTrue) { - host = "deeplink host" //that can be anything for example your package name, app/company url. (Not your supabase url) + host = "deeplink host" // this can be anything, eg. your package name or app/company url (not your supabase url) scheme = "deeplink scheme" } ``` From d594849554a354eb7e2d1a200e6ea489f7e795af Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Tue, 18 Jul 2023 19:12:48 +0800 Subject: [PATCH 08/80] fix: add check for integration installation on marketplace flow --- package-lock.json | 360 +++++++++---------- studio/pages/integrations/vercel/install.tsx | 249 ++++++++----- 2 files changed, 346 insertions(+), 263 deletions(-) diff --git a/package-lock.json b/package-lock.json index e67029b1b73..36fb978643b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4362,6 +4362,36 @@ "@mdx-js/react": "*" } }, + "node_modules/@next/swc-android-arm-eabi": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.2.tgz", + "integrity": "sha512-r2rrz+DZ8YYGqzVrbRrpP6GKzwozpOrnFbErc4k36vUTSFMag9yQahZfaBe06JYdqu/e5yhm/saIDEaSVPRP4g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-android-arm64": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.2.tgz", + "integrity": "sha512-B+TINJhCf+CrY1+b3/JWQlkecv53rAGa/gA7gi5B1cnBa/2Uvoe+Ue0JeCefTjfiyl1ScsyNx+NcESY8Ye2Ngg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-darwin-arm64": { "version": "12.3.2", "cpu": [ @@ -4376,6 +4406,156 @@ "node": ">= 10" } }, + "node_modules/@next/swc-darwin-x64": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.2.tgz", + "integrity": "sha512-1HkjmS9awwlaeEY8Y01nRSNkSv3y+qnC/mjMPe/W66hEh3QKa/LQHqHeS7NOdEs19B2mhZ7w+EgMRXdLQ0Su8w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-freebsd-x64": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.2.tgz", + "integrity": "sha512-h5Mx0BKDCJ5Vu/U8e07esF6PjPv1EJgmRbYWTUZMAflu13MQpCJkKEJir7+BeRfTXRfgFf+llc7uocrpd7mcrg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm-gnueabihf": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.2.tgz", + "integrity": "sha512-EuRZAamoxfe/WoWRaC0zsCAoE4gs/mEhilcloNM4J5Mnb3PLY8PZV394W7t5tjBjItMCF7l2Ebwjwtm46tq2RA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.2.tgz", + "integrity": "sha512-T9GCFyOIb4S3acA9LqflUYD+QZ94iZketHCqKdoO0Nx0OCHIgGJV5rotDe8TDXwh/goYpIfyHU4j1qqw4w4VnA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.2.tgz", + "integrity": "sha512-hxNVZS6L3c2z3l9EH2GP0MGQ9exu6O8cohYNZyqC9WUl6C03sEn8xzDH1y+NgD3fVurvYkGU5F0PDddJJLfDIw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.2.tgz", + "integrity": "sha512-fCPkLuwDwY8/QeXxciJJjDHG09liZym/Bhb4A+RLFQ877wUkwFsNWDUTSdUx0YXlYK/1gf67BKauqKkOKp6CYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.2.tgz", + "integrity": "sha512-o+GifBIQ2K+/MEFxHsxUZoU3bsuVFLXZYWd3idimFHiVdDCVYiKsY6mYMmKDlucX+9xRyOCkKL9Tjf+3tuXJpw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.2.tgz", + "integrity": "sha512-crii66irzGGMSUR0L8r9+A06eTv7FTXqw4rgzJ33M79EwQJOdpY7RVKXLQMurUhniEeQEEOfamiEdPIi/qxisw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.2.tgz", + "integrity": "sha512-5hRUSvn3MdQ4nVRu1rmKxq5YJzpTtZfaC/NyGw6wa4NSF1noUn/pdQGUr+I5Qz3CZkd1gZzzC0eaXQHlrk0E2g==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.2.tgz", + "integrity": "sha512-tpQJYUH+TzPMIsdVl9fH8uDg47iwiNjKY+8e9da3dXqlkztKzjSw0OwSADoqh3KrifplXeKSta+BBGLdBqg3sg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -42261,186 +42441,6 @@ "engines": { "node": ">=14.17" } - }, - "node_modules/@next/swc-android-arm-eabi": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.2.tgz", - "integrity": "sha512-r2rrz+DZ8YYGqzVrbRrpP6GKzwozpOrnFbErc4k36vUTSFMag9yQahZfaBe06JYdqu/e5yhm/saIDEaSVPRP4g==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-android-arm64": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.2.tgz", - "integrity": "sha512-B+TINJhCf+CrY1+b3/JWQlkecv53rAGa/gA7gi5B1cnBa/2Uvoe+Ue0JeCefTjfiyl1ScsyNx+NcESY8Ye2Ngg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.2.tgz", - "integrity": "sha512-1HkjmS9awwlaeEY8Y01nRSNkSv3y+qnC/mjMPe/W66hEh3QKa/LQHqHeS7NOdEs19B2mhZ7w+EgMRXdLQ0Su8w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-freebsd-x64": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.2.tgz", - "integrity": "sha512-h5Mx0BKDCJ5Vu/U8e07esF6PjPv1EJgmRbYWTUZMAflu13MQpCJkKEJir7+BeRfTXRfgFf+llc7uocrpd7mcrg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm-gnueabihf": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.2.tgz", - "integrity": "sha512-EuRZAamoxfe/WoWRaC0zsCAoE4gs/mEhilcloNM4J5Mnb3PLY8PZV394W7t5tjBjItMCF7l2Ebwjwtm46tq2RA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.2.tgz", - "integrity": "sha512-T9GCFyOIb4S3acA9LqflUYD+QZ94iZketHCqKdoO0Nx0OCHIgGJV5rotDe8TDXwh/goYpIfyHU4j1qqw4w4VnA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.2.tgz", - "integrity": "sha512-hxNVZS6L3c2z3l9EH2GP0MGQ9exu6O8cohYNZyqC9WUl6C03sEn8xzDH1y+NgD3fVurvYkGU5F0PDddJJLfDIw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.2.tgz", - "integrity": "sha512-fCPkLuwDwY8/QeXxciJJjDHG09liZym/Bhb4A+RLFQ877wUkwFsNWDUTSdUx0YXlYK/1gf67BKauqKkOKp6CYw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.2.tgz", - "integrity": "sha512-o+GifBIQ2K+/MEFxHsxUZoU3bsuVFLXZYWd3idimFHiVdDCVYiKsY6mYMmKDlucX+9xRyOCkKL9Tjf+3tuXJpw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.2.tgz", - "integrity": "sha512-crii66irzGGMSUR0L8r9+A06eTv7FTXqw4rgzJ33M79EwQJOdpY7RVKXLQMurUhniEeQEEOfamiEdPIi/qxisw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.2.tgz", - "integrity": "sha512-5hRUSvn3MdQ4nVRu1rmKxq5YJzpTtZfaC/NyGw6wa4NSF1noUn/pdQGUr+I5Qz3CZkd1gZzzC0eaXQHlrk0E2g==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.2.tgz", - "integrity": "sha512-tpQJYUH+TzPMIsdVl9fH8uDg47iwiNjKY+8e9da3dXqlkztKzjSw0OwSADoqh3KrifplXeKSta+BBGLdBqg3sg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/studio/pages/integrations/vercel/install.tsx b/studio/pages/integrations/vercel/install.tsx index 4129fd16561..2c372e609c7 100644 --- a/studio/pages/integrations/vercel/install.tsx +++ b/studio/pages/integrations/vercel/install.tsx @@ -8,10 +8,33 @@ import { useVercelIntegrationCreateMutation } from 'data/integrations/vercel-int import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useStore } from 'hooks' import { useRouter } from 'next/router' -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useVercelIntegrationInstallationState } from 'state/vercel-integration-installation' import { NextPageWithLayout, Organization } from 'types' -import { Alert, Badge, Button, IconBook, IconHexagon, IconLifeBuoy, Listbox, LoadingLine } from 'ui' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Badge, + Button, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandInput_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + Command_Shadcn_, + IconAlertTriangle, + IconBook, + IconChevronDown, + IconHexagon, + IconInfo, + IconLifeBuoy, + Listbox, + LoadingLine, + PopoverContent_Shadcn_, + PopoverTrigger_Shadcn_, + Popover_Shadcn_, +} from 'ui' /** * Organization type with `installationInstalled` added @@ -34,7 +57,9 @@ const VercelIntegration: NextPageWithLayout = () => { const router = useRouter() const { ui } = useStore() const { code, configurationId, next, teamId, source, externalId } = useParams() - const [selectedOrg, setSelectedOrg] = useState(null) + const [selectedOrg, setSelectedOrg] = useState( + null + ) const [organizationIntegrationId, setOrganizationIntegrationId] = useState(null) const snapshot = useVercelIntegrationInstallationState() @@ -48,27 +73,44 @@ const VercelIntegration: NextPageWithLayout = () => { */ const { data: integrationData, isLoading: integrationDataLoading } = useIntegrationsQuery() - const { data: organizationsData, isLoading: isLoadingOrganizationsQuery } = useOrganizationsQuery( - { - onSuccess(organizations) { - const firstOrg = organizations?.[0] - if (firstOrg && selectedOrg === null) { - setSelectedOrg(firstOrg) - snapshot.setSelectedOrganizationSlug(firstOrg.slug) - router.query.organizationSlug = firstOrg.slug - } - }, + const { data: organizationsData, isLoading: isLoadingOrganizationsQuery } = + useOrganizationsQuery() + + useEffect(() => { + if (organizationsData !== undefined && integrationData !== undefined) { + const firstOrg = organizationsWithInstalls(organizationsData)?.[0] + console.log('firstOrg', firstOrg) + + if (firstOrg && selectedOrg === null) { + setSelectedOrg(firstOrg) + snapshot.setSelectedOrganizationSlug(firstOrg.slug) + router.query.organizationSlug = firstOrg.slug + } } - ) + }, [organizationsData, integrationData]) /** - * Flat array of org slugs that have integration installed - * + * Find installedConnections */ - const flatInstalledConnectionsIds: string[] | [] = - integrationData && integrationData.length > 0 - ? integrationData?.map((x) => x.organization.slug) + function organizationsWithInstalls(organizations?: OrganizationsResponseWithInstalledData[]) { + /** + * Flat array of org slugs that have integration installed + * + */ + const flatInstalledConnectionsIds: string[] | [] = + integrationData && integrationData.length > 0 + ? integrationData?.map((x) => x.organization.slug) + : [] + + return organizations + ? organizations?.map((org) => { + return { + ...org, + installationInstalled: flatInstalledConnectionsIds.includes(org.slug) ? true : false, + } + }) : [] + } /** * Organizations with extra `installationInstalled` attribute @@ -76,14 +118,8 @@ const VercelIntegration: NextPageWithLayout = () => { * Used to show label/badge and allow/disallow installing * */ - const organizationsWithInstalledData: OrganizationsResponseWithInstalledData[] = organizationsData - ? organizationsData.map((org) => { - return { - ...org, - installationInstalled: flatInstalledConnectionsIds.includes(org.slug) ? true : false, - } - }) - : [] + const organizationsWithInstalledData: OrganizationsResponseWithInstalledData[] = + organizationsWithInstalls(organizationsData) /** * Handle the correct route change based on whether the vercel integration @@ -168,6 +204,13 @@ const VercelIntegration: NextPageWithLayout = () => { const dataLoading = isLoadingVercelIntegrationCreateMutation || isLoadingOrganizationsQuery + const disableInstallationForm = + isLoadingVercelIntegrationCreateMutation || + // disables installation button if integration is already installed and it is Marketplace flow + (selectedOrg?.installationInstalled && source === 'marketplace') + + console.log('selectedOrg', selectedOrg) + return ( <>
@@ -181,17 +224,36 @@ const VercelIntegration: NextPageWithLayout = () => { { - router.query.organizationSlug = e.slug - setSelectedOrg(e) + onSelectedOrgChange={(e: string) => { + console.log('e:', e) + + const org = organizationsWithInstalledData?.find((org) => org.slug === e) + console.log('const:', org) + + if (org) { + setSelectedOrg(org) + router.query.organizationSlug = org.slug + } }} + selectedOrg={selectedOrg} dataLoading={dataLoading} /> + {disableInstallationForm && ( + + + + Vercel Integration is already installed. + + + You will need to choose another organization to install the integration. + + + )}
+ + + + + + No results found. + + {organizationsWithInstalledData?.map((org) => { + return ( + { + if (slug) onSelectedOrgChange(slug) + setOpen(false) + }} + > + + {org.name}{' '} + {org?.installationInstalled && ( + Integration Installed + )} + + ) + })} + + + + + + ) } From d19d0ec432f42eb6968070e1538811899541c6c6 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Tue, 18 Jul 2023 22:39:18 +0800 Subject: [PATCH 09/80] chore: started moving project linker to combobox --- .../interfaces/Integrations/ProjectLinker.tsx | 178 +++++++++++++++++- studio/pages/integrations/vercel/install.tsx | 4 +- 2 files changed, 179 insertions(+), 3 deletions(-) diff --git a/studio/components/interfaces/Integrations/ProjectLinker.tsx b/studio/components/interfaces/Integrations/ProjectLinker.tsx index 41cf88e40ed..3c03dbf58f4 100644 --- a/studio/components/interfaces/Integrations/ProjectLinker.tsx +++ b/studio/components/interfaces/Integrations/ProjectLinker.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useRef, useState } from 'react' import { toast } from 'react-hot-toast' import { ENV_VAR_RAW_KEYS } from 'components/interfaces/Integrations/Integrations-Vercel.constants' @@ -10,7 +10,22 @@ import { VercelProjectsResponse } from 'data/integrations/integrations-vercel-pr import { IntegrationProjectConnection } from 'data/integrations/integrations.types' import { useSelectedOrganization } from 'hooks' import { BASE_PATH } from 'lib/constants' -import { Button, Listbox, cn } from 'ui' +import { + Button, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandInput_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + Command_Shadcn_, + IconChevronDown, + IconHexagon, + Listbox, + PopoverContent_Shadcn_, + PopoverTrigger_Shadcn_, + Popover_Shadcn_, + cn, +} from 'ui' interface Project { id: string @@ -39,6 +54,11 @@ const ProjectLinker = ({ setLoading, showSkip = false, }: ProjectLinkerProps) => { + const [supabaseProjectsComboBoxOpen, setSupabaseProjectsComboboxOpen] = useState(false) + const [vercelProjectsComboBoxOpen, setVercelProjectsComboboxOpen] = useState(false) + const supabaseProjectsComboBoxRef = useRef(null) + const vercelProjectsComboBoxRef = useRef(null) + const selectedOrganization = useSelectedOrganization() const [supabaseProjectRef, setSupabaseProjectRef] = useState(UNDEFINED_SELECT_VALUE) @@ -125,6 +145,80 @@ const ProjectLinker = ({
Supabase
+ + + +
+ } + iconRight={ + + + + } + > + + {supabaseProjectRef ?? UNDEFINED_SELECT_VALUE} + {/* {selectedOrg?.installationInstalled && ( + Integration Installed + )} */} + + + + + + + + No results found. + + {supabaseProjects.map((project) => { + return ( + { + if (ref) setSupabaseProjectRef(ref) + setSupabaseProjectsComboboxOpen(false) + }} + > +
+ Supabase +
+ {project.name} +
+ ) + })} +
+
+
+
+ + + + + + + + + + + + No results found. + + {filteredForeignProjects.map((project) => { + return ( + { + if (ref) setSupabaseProjectRef(ref) + setSupabaseProjectsComboboxOpen(false) + }} + > + {!project?.framework ? ( + vercelIcon + ) : ( + {`icon`} + )} + {project.name} + + ) + })} + + + + + + { const disableInstallationForm = isLoadingVercelIntegrationCreateMutation || // disables installation button if integration is already installed and it is Marketplace flow - (selectedOrg?.installationInstalled && source === 'marketplace') + (selectedOrg && selectedOrg?.installationInstalled && source === 'marketplace') + ? true + : false console.log('selectedOrg', selectedOrg) From 49914f55719911189905f016815c8dd83732422e Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Wed, 19 Jul 2023 13:56:23 +0200 Subject: [PATCH 10/80] Revert "Revert "feat: add twilio verify docs"" --- .../pages/guides/auth/phone-login/twilio.mdx | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/apps/docs/pages/guides/auth/phone-login/twilio.mdx b/apps/docs/pages/guides/auth/phone-login/twilio.mdx index d4bae16558c..a22a16d7f95 100644 --- a/apps/docs/pages/guides/auth/phone-login/twilio.mdx +++ b/apps/docs/pages/guides/auth/phone-login/twilio.mdx @@ -7,16 +7,42 @@ export const meta = { video: 'https://www.youtube.com/v/akScoPO01bc', } -## Overview - In this guide we'll show you how to authenticate your users with SMS based One-Time Password (OTP) tokens. There are two reasons to use Supabase SMS OTP tokens: -- You want users to log in with mobile number + password, and the mobile number should be verified via SMS -- You want users to log in with mobile number ONLY (i.e. passwordless login) +- You want users to log in with a phone number and password, and verify the phone number on signup with SMS +- You want users to log in with a phone number ONLY (i.e. passwordless login) -We'll cover: +What you'll need: + +- Twilio account ([sign up](https://www.twilio.com/try-twilio)) +- Supabase project (create one [here](https://supabase.com/dashboard)) +- Mobile phone capable of receiving SMS + +SMS Authentication can be done with either Twilio Verify or Twilio Programmable Messaging. [Twilio Verify](https://www.twilio.com/en-us/trusted-activation/verify) is a specialized OTP solution and is recommended for most developers that need over-the-phone authentication. Alternatively you can use [Twilio Programmable Messaging](https://www.twilio.com/docs/messaging) which offers generic SMS sending support. + +## Twilio Verify + +To set up Twilio Verify, you will need to: + +1. Create a new [verification service](https://support.twilio.com/hc/en-us/articles/360033309133-Getting-Started-with-Twilio-Verify-V2) in the Twilio dashboard. +2. [Switch Phone Provider to Twilio Verify](https://supabase.com/dashboard/project/_/auth/providers) +3. Configure the Message Service ID field using the Verification Service ID obtained in 1. + +When using Twilio Verify, OTPs are generated by Twilio. This means that: + +- Unlike other providers, the OTP expiry duration and message content fields are not configurable via the Supabase dashboard. Please head to Twilio Verify to configure these settings. +- The token remains the same during its validity period until the verification is successful. This means if your user make another request within that period, they will receive the same token. +- Twilio Verify has a separate set of rate limits that apply. Visit Twilio's [Rate Limit and Timeouts page](https://www.twilio.com/docs/verify/api/rate-limits-and-timeouts) to find out more. + + + At this time, Twilio Verify is only supported on the `whatsapp` and `sms` channels. + + +## Twilio (Programmable Messaging) + +In this section we'll cover: - [Finding your Twilio credentials](#finding-your-twilio-credentials) - [Using OTP with password based logins](#using-otp-with-password-based-logins) @@ -349,6 +375,8 @@ let { data, error } = await supabase.auth.verifyOtp({ ## Resources +- [Reasons to use Twilio Verify](https://www.twilio.com/blog/9-reasons-to-use-the-verify-api) +- [Twilio Verify Documentation](https://www.twilio.com/docs/verify) - [Twilio Signup](https://www.twilio.com/try-twilio) - [Supabase Dashboard](https://supabase.com/dashboard) - [Supabase Row Level Security](/docs/guides/auth#row-level-security) From 0ff11990a41e3dc2f4092fd0bb733ebd03a2b933 Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Wed, 19 Jul 2023 15:06:59 +0200 Subject: [PATCH 11/80] Update docs --- spec/supabase_kt_v0.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/supabase_kt_v0.yml b/spec/supabase_kt_v0.yml index 20ded9e5963..ebda4824eee 100644 --- a/spec/supabase_kt_v0.yml +++ b/spec/supabase_kt_v0.yml @@ -3356,13 +3356,13 @@ functions: notes: | Returns all Realtime channels. examples: - - id: get-all-channels - name: Get all channels - isSpotlight: true - code: | - ```kotlin - val channels = client.realtime.subscriptions.entries - ``` + - id: get-all-channels + name: Get all channels + isSpotlight: true + code: | + ```kotlin + val channels = client.realtime.subscriptions.entries + ``` - id: list-buckets title: listBuckets() $ref: '@supabase/storage-js.packages/StorageBucketApi.default.listBuckets' From 2555dd8294f632960e57414b9ad5cd7868882da3 Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Wed, 19 Jul 2023 19:40:02 +0200 Subject: [PATCH 12/80] Format docs --- apps/docs/docs/ref/kotlin/introduction.mdx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/docs/docs/ref/kotlin/introduction.mdx b/apps/docs/docs/ref/kotlin/introduction.mdx index 6dd0279006a..914310ba347 100644 --- a/apps/docs/docs/ref/kotlin/introduction.mdx +++ b/apps/docs/docs/ref/kotlin/introduction.mdx @@ -19,16 +19,16 @@ This reference documents every object and method available in Supabase's Kotlin Supported Kotlin targets: | | **GoTrue** | **Realtime** | **Postgrest** | **Storage** | **Functions** | **Apollo-GraphQL** | -|--------------------------------------------------------------------|------------|--------------|---------------|-------------|---------------|--------------------| -| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Android** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **JS** *(Browser, NodeJS)* | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **IOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **tvOS** *(tvosArm64, tvosX64, tvosSimulatorArm64)* 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **watchOS** *(watchosArm64, watchosX64, watchosSimulatorArm64)* 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **MacOS** *(macosX64 & macosArm64)* 🚧 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Windows** *(mingwX64)* 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | -| **Linux** *(linuxX64)* 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | +| ------------------------------------------------------------------ | ---------- | ------------ | ------------- | ----------- | ------------- | ------------------ | +| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Android** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **JS** _(Browser, NodeJS)_ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **IOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **tvOS** _(tvosArm64, tvosX64, tvosSimulatorArm64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **watchOS** _(watchosArm64, watchosX64, watchosSimulatorArm64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **MacOS** _(macosX64 & macosArm64)_ 🚧 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Windows** _(mingwX64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | +| **Linux** _(linuxX64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ = full support From 8dc302904335a557199aa2147a3e2777b0176a17 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Thu, 20 Jul 2023 11:21:20 +0800 Subject: [PATCH 13/80] Update package-lock.json --- package-lock.json | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 36fb978643b..b4b6092e178 100644 --- a/package-lock.json +++ b/package-lock.json @@ -324,7 +324,6 @@ "gray-matter": "^4.0.3", "lottie-react": "^2.4.0", "markdown-toc": "^1.2.0", - "marked": "^4.0.12", "next": "^12.3.2", "next-compose-plugins": "^2.2.1", "next-mdx-remote": "^4.1.0", @@ -361,7 +360,6 @@ "@tailwindcss/typography": "^0.5.9", "@types/classnames": "^2.3.1", "@types/jest": "^27.4.0", - "@types/marked": "^4.0.3", "@types/mdx-js__react": "^1.5.5", "@types/node": "^17.0.24", "@types/nodemailer": "^6.4.4", @@ -14291,11 +14289,6 @@ "markdown-table": "*" } }, - "node_modules/@types/marked": { - "version": "4.3.1", - "dev": true, - "license": "MIT" - }, "node_modules/@types/mdast": { "version": "3.0.11", "license": "MIT", @@ -26680,16 +26673,6 @@ "node": ">=0.10.0" } }, - "node_modules/marked": { - "version": "4.3.0", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, "node_modules/math-random": { "version": "1.0.4", "license": "MIT" @@ -39696,8 +39679,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "license": "MIT", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "engines": { "node": ">=0.10.0" } @@ -39928,7 +39912,7 @@ "license": "MIT", "dependencies": { "@headlessui/react": "^1.7.14", - "@supabase/gotrue-js": "^2.35.0", + "@supabase/gotrue-js": "^2.44.0", "@supabase/ui": "^0.37.0-alpha.50", "react-use": "^17.4.0" }, @@ -39941,9 +39925,9 @@ } }, "packages/common/node_modules/@supabase/gotrue-js": { - "version": "2.35.0", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.35.0.tgz", - "integrity": "sha512-bLNIrBzFNRR61qUN8VZ4iV7q+fPilWyYQi0CZlxuHBiPtVcNPTul02DbNvgvw76hNz+mB5maay/Y17lSHnSRmw==", + "version": "2.44.0", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.44.0.tgz", + "integrity": "sha512-Et7/UVYsEvMTMEA+F26Vxsqvnq79dG1H5bq31WkyQXH3hY0JUA0+BUixlVinG5Uy/2SRk5eZOs4xxDmQQEJAFw==", "dependencies": { "cross-fetch": "^3.1.5" } From 24ed26817bb3594504dbb2e83daf7605a9ab4835 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Thu, 20 Jul 2023 11:53:23 +0800 Subject: [PATCH 14/80] Update Scaffold.tsx --- studio/components/layouts/Scaffold.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/studio/components/layouts/Scaffold.tsx b/studio/components/layouts/Scaffold.tsx index 92c695d0e85..aec55712385 100644 --- a/studio/components/layouts/Scaffold.tsx +++ b/studio/components/layouts/Scaffold.tsx @@ -4,6 +4,8 @@ import { cn } from 'ui' const maxWidthClasses = 'mx-auto w-full max-w-[1600px]' const paddingClasses = 'px-6 lg:px-14 xl:px-28 2xl:px-32' +const maxWidthClassesColumn = 'min-w-[420px]' + // lg: pt - 8 const ScaffoldHeader = React.forwardRef>( @@ -45,6 +47,18 @@ const ScaffoldSection = React.forwardRef>( + ({ className, ...props }, ref) => { + return ( +
+ ) + } +) + const ScaffoldSectionDetail = React.forwardRef< HTMLDivElement, React.HTMLAttributes @@ -107,6 +121,7 @@ ScaffoldTitle.displayName = 'ScaffoldTitle' ScaffoldContainer.displayName = 'ScaffoldContainer' ScaffoldDivider.displayName = 'ScaffoldDivider' ScaffoldSection.displayName = 'ScaffoldSection' +ScaffoldColumn.displayName = 'ScaffoldColumn' ScaffoldSectionDetail.displayName = 'ScaffoldSectionDetail' ScaffoldSectionContent.displayName = 'ScaffoldSectionContent' ScaffoldFilterAndContent.displayName = 'ScaffoldFilterAndContent' @@ -120,6 +135,7 @@ export { ScaffoldContainer, ScaffoldDivider, ScaffoldSection, + ScaffoldColumn, ScaffoldSectionDetail, ScaffoldSectionContent, ScaffoldFilterAndContent, From c5b31637e1573a74b65b4864bd90efdbae1cc1bd Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Thu, 20 Jul 2023 15:48:47 +0800 Subject: [PATCH 15/80] fix colors --- .../interfaces/Integrations/IntegrationPanels.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/studio/components/interfaces/Integrations/IntegrationPanels.tsx b/studio/components/interfaces/Integrations/IntegrationPanels.tsx index e4e6d36cb9f..e84debd1f2c 100644 --- a/studio/components/interfaces/Integrations/IntegrationPanels.tsx +++ b/studio/components/interfaces/Integrations/IntegrationPanels.tsx @@ -79,7 +79,7 @@ const IntegrationInstallation = React.forwardRef
@@ -145,7 +145,7 @@ const IntegrationConnection = React.forwardRef
-
+
@@ -193,7 +193,9 @@ const IntegrationConnectionOption = React.forwardRef
From 57b7cafc22cc2126327696f577cf8e29e9f03116 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Thu, 20 Jul 2023 16:10:44 +0800 Subject: [PATCH 16/80] chore: use comboboxes. fix: issues with validation and loading states --- .../interfaces/Integrations/ProjectLinker.tsx | 174 +++++++----------- .../layouts/IntegrationWindowLayout.tsx | 2 +- studio/components/layouts/Scaffold.tsx | 2 +- .../[slug]/marketplace/choose-project.tsx | 71 +++---- studio/pages/integrations/vercel/install.tsx | 115 ++++++------ 5 files changed, 159 insertions(+), 205 deletions(-) diff --git a/studio/components/interfaces/Integrations/ProjectLinker.tsx b/studio/components/interfaces/Integrations/ProjectLinker.tsx index 3c03dbf58f4..d0b66326a9c 100644 --- a/studio/components/interfaces/Integrations/ProjectLinker.tsx +++ b/studio/components/interfaces/Integrations/ProjectLinker.tsx @@ -1,6 +1,3 @@ -import { useRef, useState } from 'react' -import { toast } from 'react-hot-toast' - import { ENV_VAR_RAW_KEYS } from 'components/interfaces/Integrations/Integrations-Vercel.constants' import { Markdown } from 'components/interfaces/Markdown' import { vercelIcon } from 'components/to-be-cleaned/ListIcons' @@ -10,6 +7,8 @@ import { VercelProjectsResponse } from 'data/integrations/integrations-vercel-pr import { IntegrationProjectConnection } from 'data/integrations/integrations.types' import { useSelectedOrganization } from 'hooks' import { BASE_PATH } from 'lib/constants' +import { useRef, useState } from 'react' +import { toast } from 'react-hot-toast' import { Button, CommandEmpty_Shadcn_, @@ -19,8 +18,6 @@ import { CommandList_Shadcn_, Command_Shadcn_, IconChevronDown, - IconHexagon, - Listbox, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, @@ -41,10 +38,10 @@ export interface ProjectLinkerProps { installedConnections: IntegrationProjectConnection[] | undefined setLoading?: (x: boolean) => void showSkip?: boolean + loadingForeignProjects?: boolean + loadingSupabaseProjects?: boolean } -const UNDEFINED_SELECT_VALUE = 'undefined' - const ProjectLinker = ({ organizationIntegrationId, foreignProjects, @@ -53,6 +50,8 @@ const ProjectLinker = ({ installedConnections = [], setLoading, showSkip = false, + loadingForeignProjects, + loadingSupabaseProjects, }: ProjectLinkerProps) => { const [supabaseProjectsComboBoxOpen, setSupabaseProjectsComboboxOpen] = useState(false) const [vercelProjectsComboBoxOpen, setVercelProjectsComboboxOpen] = useState(false) @@ -61,8 +60,8 @@ const ProjectLinker = ({ const selectedOrganization = useSelectedOrganization() - const [supabaseProjectRef, setSupabaseProjectRef] = useState(UNDEFINED_SELECT_VALUE) - const [vercelProjectId, setVercelProjectId] = useState(UNDEFINED_SELECT_VALUE) + const [supabaseProjectRef, setSupabaseProjectRef] = useState(undefined) + const [vercelProjectId, setVercelProjectId] = useState(undefined) const { mutateAsync: syncEnvs } = useIntegrationsVercelConnectionSyncEnvsMutation() const { mutate: createConnections, isLoading } = useIntegrationConnectionsCreateMutation({ @@ -113,7 +112,7 @@ const ProjectLinker = ({ return (
x.ref?.toLowerCase() === supabaseProjectRef?.toLowerCase()) + : undefined + const selectedVercelProject = vercelProjectId + ? filteredForeignProjects.find((x) => x.id?.toLowerCase() === vercelProjectId?.toLowerCase()) + : undefined + + // console.log('selectedSupabaseProject', selectedSupabaseProject) + console.log('filteredForeignProjects', filteredForeignProjects) + console.log('vercelProjectId', vercelProjectId) + console.log('selectedVercelProject', selectedVercelProject) + + // console.log('supabaseProjects', supabaseProjects) + return (
@@ -156,6 +169,8 @@ const ProjectLinker = ({ type="default" size="medium" block + disabled={loadingSupabaseProjects} + loading={loadingSupabaseProjects} className="justify-start" icon={
@@ -172,12 +187,7 @@ const ProjectLinker = ({ } > - - {supabaseProjectRef ?? UNDEFINED_SELECT_VALUE} - {/* {selectedOrg?.installationInstalled && ( - Integration Installed - )} */} - + {selectedSupabaseProject ? selectedSupabaseProject.name : 'Choose Project'} - + No results found. {supabaseProjects.map((project) => { @@ -218,38 +228,6 @@ const ProjectLinker = ({ - - setSupabaseProjectRef(e)} - > - - Choose project - - {supabaseProjects.map((project) => ( - { - return ( - <> -
- Supabase -
- - ) - }} - > - {project.name} -
- ))} -
@@ -271,46 +249,48 @@ const ProjectLinker = ({ > - + No results found. {filteredForeignProjects.map((project) => { @@ -319,9 +299,10 @@ const ProjectLinker = ({ value={project.id} key={project.id} className="flex gap-2 items-center" - onSelect={(ref) => { - if (ref) setSupabaseProjectRef(ref) - setSupabaseProjectsComboboxOpen(false) + onSelect={(id) => { + console.log('id set', id) + if (id) setVercelProjectId(id) + setVercelProjectsComboboxOpen(false) }} > {!project?.framework ? ( @@ -343,43 +324,6 @@ const ProjectLinker = ({ - - setVercelProjectId(e)} - > - - Choose Vercel project - - {filteredForeignProjects.map((project) => { - return ( - { - return ( - <> - {!project?.framework ? ( - vercelIcon - ) : ( - {`icon`} - )} - - ) - }} - > - {project.name} - - ) - })} -
@@ -400,7 +344,15 @@ const ProjectLinker = ({ className="self-end" onClick={onCreateConnections} loading={isLoading} - disabled={isLoading} + disabled={ + // data loading states + loadingForeignProjects || + loadingSupabaseProjects || + isLoading || + // check wether both project types are not undefined + !selectedSupabaseProject || + !selectedVercelProject + } > Connect project diff --git a/studio/components/layouts/IntegrationWindowLayout.tsx b/studio/components/layouts/IntegrationWindowLayout.tsx index 123e42f205c..48391679396 100644 --- a/studio/components/layouts/IntegrationWindowLayout.tsx +++ b/studio/components/layouts/IntegrationWindowLayout.tsx @@ -36,7 +36,7 @@ const Header: FC = ({ organization, project }) => { return (
- +
diff --git a/studio/components/layouts/Scaffold.tsx b/studio/components/layouts/Scaffold.tsx index aec55712385..bcf1cfd9bd3 100644 --- a/studio/components/layouts/Scaffold.tsx +++ b/studio/components/layouts/Scaffold.tsx @@ -53,7 +53,7 @@ const ScaffoldColumn = React.forwardRef ) } diff --git a/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx b/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx index bbb7dbd0b86..fa67ca265bb 100644 --- a/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx +++ b/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx @@ -4,7 +4,7 @@ import { useParams } from 'common' import ProjectLinker from 'components/interfaces/Integrations/ProjectLinker' import { Markdown } from 'components/interfaces/Markdown' import IntegrationWindowLayout from 'components/layouts/IntegrationWindowLayout' -import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold' +import { ScaffoldColumn, ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold' import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only' import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-projects-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' @@ -35,9 +35,11 @@ const VercelIntegration: NextPageWithLayout = () => { const integration = integrationData?.find((x) => x.metadata?.configuration_id === configurationId) - const { data: supabaseProjectsData } = useProjectsQuery({ - enabled: integration?.id !== undefined, - }) + const { data: supabaseProjectsData, isLoading: isLoadingSupabaseProjectsData } = useProjectsQuery( + { + enabled: integration?.id !== undefined, + } + ) const supabaseProjects = useMemo( () => @@ -48,45 +50,50 @@ const VercelIntegration: NextPageWithLayout = () => { [organizationSlug, supabaseProjectsData] ) - const { data: vercelProjectsData } = useVercelProjectsQuery( - { - organization_integration_id: integration?.id, - }, - { enabled: integration?.id !== undefined } - ) + const { data: vercelProjectsData, isLoading: isLoadingVercelProjectsData } = + useVercelProjectsQuery( + { + organization_integration_id: integration?.id, + }, + { enabled: integration?.id !== undefined } + ) const vercelProjects = useMemo(() => vercelProjectsData ?? EMPTY_ARR, [vercelProjectsData]) return ( <> +
- <> -
-

- Link a Supabase project to a Vercel project -

- +
+

+ Link a Supabase project to a Vercel project +

+ +
+ { + if (next) { + window.location.href = next + } + }} + installedConnections={integration?.connections} + setLoading={setLoading} + showSkip={true} + loadingForeignProjects={isLoadingVercelProjectsData} + loadingSupabaseProjects={isLoadingSupabaseProjectsData} /> -
- { - if (next) { - window.location.href = next - } - }} - installedConnections={integration?.connections} - setLoading={setLoading} - showSkip={true} - /> +
diff --git a/studio/pages/integrations/vercel/install.tsx b/studio/pages/integrations/vercel/install.tsx index 0dd64d5af11..28b4b5017c8 100644 --- a/studio/pages/integrations/vercel/install.tsx +++ b/studio/pages/integrations/vercel/install.tsx @@ -1,7 +1,7 @@ import { useParams } from 'common' import { Markdown } from 'components/interfaces/Markdown' import IntegrationWindowLayout from 'components/layouts/IntegrationWindowLayout' -import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold' +import { ScaffoldColumn, ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold' import { useIntegrationsQuery } from 'data/integrations/integrations-query' import { IntegrationName } from 'data/integrations/integrations.types' import { useVercelIntegrationCreateMutation } from 'data/integrations/vercel-integration-create-mutation' @@ -29,7 +29,6 @@ import { IconHexagon, IconInfo, IconLifeBuoy, - Listbox, LoadingLine, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, @@ -205,9 +204,9 @@ const VercelIntegration: NextPageWithLayout = () => { const dataLoading = isLoadingVercelIntegrationCreateMutation || isLoadingOrganizationsQuery const disableInstallationForm = - isLoadingVercelIntegrationCreateMutation || + (isLoadingVercelIntegrationCreateMutation && !dataLoading) || // disables installation button if integration is already installed and it is Marketplace flow - (selectedOrg && selectedOrg?.installationInstalled && source === 'marketplace') + (selectedOrg && selectedOrg?.installationInstalled && source === 'marketplace' && !dataLoading) ? true : false @@ -219,50 +218,52 @@ const VercelIntegration: NextPageWithLayout = () => { {organizationIntegrationId === null && ( <> - -

Choose organization

- <> - - { - console.log('e:', e) + + +

Choose organization

+ <> + + { + console.log('e:', e) - const org = organizationsWithInstalledData?.find((org) => org.slug === e) - console.log('const:', org) + const org = organizationsWithInstalledData?.find((org) => org.slug === e) + console.log('const:', org) - if (org) { - setSelectedOrg(org) - router.query.organizationSlug = org.slug - } - }} - selectedOrg={selectedOrg} - dataLoading={dataLoading} - /> - {disableInstallationForm && ( - - - - Vercel Integration is already installed. - - - You will need to choose another organization to install the integration. - - - )} -
- -
- + if (org) { + setSelectedOrg(org) + router.query.organizationSlug = org.slug + } + }} + selectedOrg={selectedOrg} + dataLoading={dataLoading} + /> + {disableInstallationForm && ( + + + + Vercel Integration is already installed. + + + You will need to choose another organization to install the integration. + + + )} +
+ +
+ +
@@ -280,7 +281,7 @@ const VercelIntegration: NextPageWithLayout = () => {
- +
Docs
@@ -313,16 +314,6 @@ const OrganizationPicker = ({ const [open, setOpen] = useState(false) const ref = useRef(null) - if (dataLoading) { - return ( - - - Loading... - - - ) - } - return ( <> @@ -334,6 +325,8 @@ const OrganizationPicker = ({ block className="justify-start" icon={} + loading={dataLoading} + disabled={dataLoading} iconRight={ @@ -341,7 +334,7 @@ const OrganizationPicker = ({ } > - {selectedOrg?.name} + {selectedOrg?.name} {selectedOrg?.installationInstalled && ( Integration Installed )} @@ -371,9 +364,11 @@ const OrganizationPicker = ({ }} > - {org.name}{' '} + {org.name}{' '} {org?.installationInstalled && ( - Integration Installed + + Integration Installed + )} ) From 2650f7b418562461b92bd5cafbf46f026b513551 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Thu, 20 Jul 2023 17:47:11 +0900 Subject: [PATCH 17/80] update menu path to the smtp setting --- apps/docs/pages/guides/platform/going-into-prod.mdx | 4 ++-- apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs/pages/guides/platform/going-into-prod.mdx b/apps/docs/pages/guides/platform/going-into-prod.mdx index c3ac2663e8c..7e94058985f 100644 --- a/apps/docs/pages/guides/platform/going-into-prod.mdx +++ b/apps/docs/pages/guides/platform/going-into-prod.mdx @@ -21,7 +21,7 @@ After developing your project and deciding it's Production Ready, you should run - Go to the Authentication > Policies page in the Supabase Dashboard to enable RLS and create security policies. - Go to the Database > Replication page in the Supabase Dashboard to manage replication tables. - Enable 2FA on GitHub. Since your GitHub account gives you administrative rights to your Supabase project, you should protect it with a strong password and 2FA using a U2F key or a TOTP app. -- Ensure email confirmations are enabled in the `Auth > Settings` page. +- Ensure email confirmations are enabled in the `Project Settings > Auth` page. - Use a custom SMTP server for auth emails so that your users can see that the mails are coming from a trusted domain (preferably the same domain that your app is hosted on). Grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. - Think hard about how _you_ would abuse your service as an attacker, and mitigate. - Review these [common cybersecurity threats](https://auth0.com/docs/security/prevent-threats). @@ -39,7 +39,7 @@ After developing your project and deciding it's Production Ready, you should run ## Availability -- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails (see Auth > Settings) +- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails (see Project Settings > Auth) - you can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. - The default rate limit for auth emails provided by Supabase is 30 new users per hour, if doing a major public announcement you will likely require more than this. - If your application is on the free plan and is **not** expected to be queried at least once every 7 days, then it may be paused by Supabase to save on server resources. diff --git a/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx b/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx index a9531106ed8..a0609294c9a 100644 --- a/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx +++ b/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx @@ -40,7 +40,7 @@ curl -X POST 'https://.supabase.co/auth/v1/magiclink' \ }' ``` -Gotrue is responsible for issuing access tokens for your users, sends confirmation, magic-link, and password recovery emails (by default we send these from a Supabase SMTP server, but you can easily plug in your own inside the dashboard at Auth > Settings) and also transacting with third party OAuth providers to get basic user data. +Gotrue is responsible for issuing access tokens for your users, sends confirmation, magic-link, and password recovery emails (by default we send these from a Supabase SMTP server, but you can easily plug in your own inside the dashboard at Project Project Settings > Auth) and also transacting with third party OAuth providers to get basic user data. The community even recently built in the functionality to request custom OAuth scopes, if your users need to interact more closely with the provider. See the scopes parameter here: [https://github.com/supabase/gotrue#get-authorize](https://github.com/supabase/gotrue#get-authorize). From bc67627ce2ecd69717d78744b59c62af7de82ff0 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Thu, 20 Jul 2023 17:51:00 +0900 Subject: [PATCH 18/80] remove project keyword --- apps/docs/pages/guides/platform/going-into-prod.mdx | 4 ++-- apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs/pages/guides/platform/going-into-prod.mdx b/apps/docs/pages/guides/platform/going-into-prod.mdx index 7e94058985f..d2852c3d7ec 100644 --- a/apps/docs/pages/guides/platform/going-into-prod.mdx +++ b/apps/docs/pages/guides/platform/going-into-prod.mdx @@ -21,7 +21,7 @@ After developing your project and deciding it's Production Ready, you should run - Go to the Authentication > Policies page in the Supabase Dashboard to enable RLS and create security policies. - Go to the Database > Replication page in the Supabase Dashboard to manage replication tables. - Enable 2FA on GitHub. Since your GitHub account gives you administrative rights to your Supabase project, you should protect it with a strong password and 2FA using a U2F key or a TOTP app. -- Ensure email confirmations are enabled in the `Project Settings > Auth` page. +- Ensure email confirmations are enabled in the `Settings > Auth` page. - Use a custom SMTP server for auth emails so that your users can see that the mails are coming from a trusted domain (preferably the same domain that your app is hosted on). Grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. - Think hard about how _you_ would abuse your service as an attacker, and mitigate. - Review these [common cybersecurity threats](https://auth0.com/docs/security/prevent-threats). @@ -39,7 +39,7 @@ After developing your project and deciding it's Production Ready, you should run ## Availability -- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails (see Project Settings > Auth) +- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails (see Settings > Auth) - you can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. - The default rate limit for auth emails provided by Supabase is 30 new users per hour, if doing a major public announcement you will likely require more than this. - If your application is on the free plan and is **not** expected to be queried at least once every 7 days, then it may be paused by Supabase to save on server resources. diff --git a/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx b/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx index a0609294c9a..85d22733009 100644 --- a/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx +++ b/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx @@ -40,7 +40,7 @@ curl -X POST 'https://.supabase.co/auth/v1/magiclink' \ }' ``` -Gotrue is responsible for issuing access tokens for your users, sends confirmation, magic-link, and password recovery emails (by default we send these from a Supabase SMTP server, but you can easily plug in your own inside the dashboard at Project Project Settings > Auth) and also transacting with third party OAuth providers to get basic user data. +Gotrue is responsible for issuing access tokens for your users, sends confirmation, magic-link, and password recovery emails (by default we send these from a Supabase SMTP server, but you can easily plug in your own inside the dashboard at Project Settings > Auth) and also transacting with third party OAuth providers to get basic user data. The community even recently built in the functionality to request custom OAuth scopes, if your users need to interact more closely with the provider. See the scopes parameter here: [https://github.com/supabase/gotrue#get-authorize](https://github.com/supabase/gotrue#get-authorize). From 46be2943f3fc6bbbbfb56750189cb7e1fbd79b85 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Fri, 21 Jul 2023 13:00:06 +0800 Subject: [PATCH 19/80] remove logs --- .../components/interfaces/Integrations/ProjectLinker.tsx | 9 --------- studio/pages/integrations/vercel/install.tsx | 6 ------ 2 files changed, 15 deletions(-) diff --git a/studio/components/interfaces/Integrations/ProjectLinker.tsx b/studio/components/interfaces/Integrations/ProjectLinker.tsx index d0b66326a9c..ef9841015d7 100644 --- a/studio/components/interfaces/Integrations/ProjectLinker.tsx +++ b/studio/components/interfaces/Integrations/ProjectLinker.tsx @@ -139,13 +139,6 @@ const ProjectLinker = ({ ? filteredForeignProjects.find((x) => x.id?.toLowerCase() === vercelProjectId?.toLowerCase()) : undefined - // console.log('selectedSupabaseProject', selectedSupabaseProject) - console.log('filteredForeignProjects', filteredForeignProjects) - console.log('vercelProjectId', vercelProjectId) - console.log('selectedVercelProject', selectedVercelProject) - - // console.log('supabaseProjects', supabaseProjects) - return (
@@ -232,7 +225,6 @@ const ProjectLinker = ({
- {/* integration icon */} { - console.log('id set', id) if (id) setVercelProjectId(id) setVercelProjectsComboboxOpen(false) }} diff --git a/studio/pages/integrations/vercel/install.tsx b/studio/pages/integrations/vercel/install.tsx index 28b4b5017c8..25fe1818e2c 100644 --- a/studio/pages/integrations/vercel/install.tsx +++ b/studio/pages/integrations/vercel/install.tsx @@ -78,7 +78,6 @@ const VercelIntegration: NextPageWithLayout = () => { useEffect(() => { if (organizationsData !== undefined && integrationData !== undefined) { const firstOrg = organizationsWithInstalls(organizationsData)?.[0] - console.log('firstOrg', firstOrg) if (firstOrg && selectedOrg === null) { setSelectedOrg(firstOrg) @@ -210,8 +209,6 @@ const VercelIntegration: NextPageWithLayout = () => { ? true : false - console.log('selectedOrg', selectedOrg) - return ( <>
@@ -227,10 +224,7 @@ const VercelIntegration: NextPageWithLayout = () => { integrationName="Vercel" organizationsWithInstalledData={organizationsWithInstalledData} onSelectedOrgChange={(e: string) => { - console.log('e:', e) - const org = organizationsWithInstalledData?.find((org) => org.slug === e) - console.log('const:', org) if (org) { setSelectedOrg(org) From 2bdb62dde10fde8ea0a61f716ee46f4de2d02c08 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Fri, 21 Jul 2023 18:22:37 +1000 Subject: [PATCH 20/80] filter to vercel integrations only --- studio/pages/integrations/vercel/install.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/studio/pages/integrations/vercel/install.tsx b/studio/pages/integrations/vercel/install.tsx index 25fe1818e2c..a323420f3d5 100644 --- a/studio/pages/integrations/vercel/install.tsx +++ b/studio/pages/integrations/vercel/install.tsx @@ -97,7 +97,9 @@ const VercelIntegration: NextPageWithLayout = () => { */ const flatInstalledConnectionsIds: string[] | [] = integrationData && integrationData.length > 0 - ? integrationData?.map((x) => x.organization.slug) + ? integrationData + .filter((x) => x.integration.name === 'Vercel') + .map((x) => x.organization.slug) : [] return organizations From 1a0e4ca06d29a35b92d9695dd065d97e26de9fd0 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 20 Jul 2023 15:51:54 +0200 Subject: [PATCH 21/80] If "uuid" is selected as column type, autoselect "gen_random_uuid()" for default value. --- .../SidePanelEditor/TableEditor/Column.tsx | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx index eeb1a84654a..cb5d9043ec6 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx @@ -1,25 +1,24 @@ -import { FC } from 'react' import * as Tooltip from '@radix-ui/react-tooltip' -import { - Checkbox, - Input, - IconX, - IconMenu, - Popover, - IconLink, - IconSettings, - Button, - IconArrowRight, -} from 'ui' import type { PostgresType } from '@supabase/postgres-meta' +import { + Button, + Checkbox, + IconArrowRight, + IconLink, + IconMenu, + IconSettings, + IconX, + Input, + Popover, +} from 'ui' -import { ColumnField } from '../SidePanelEditor.types' +import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants' +import { typeExpressionSuggestions } from '../ColumnEditor/ColumnEditor.constants' +import { Suggestion } from '../ColumnEditor/ColumnEditor.types' +import { getForeignKeyDeletionAction } from '../ColumnEditor/ColumnEditor.utils' import ColumnType from '../ColumnEditor/ColumnType' import InputWithSuggestions from '../ColumnEditor/InputWithSuggestions' -import { Suggestion } from '../ColumnEditor/ColumnEditor.types' -import { typeExpressionSuggestions } from '../ColumnEditor/ColumnEditor.constants' -import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants' -import { getForeignKeyDeletionAction } from '../ColumnEditor/ColumnEditor.utils' +import { ColumnField } from '../SidePanelEditor.types' /** * [Joshen] For context: @@ -50,7 +49,7 @@ interface Props { onRemoveColumn: () => void } -const Column: FC = ({ +const Column = ({ column = {} as ColumnField, enumTypes = [] as PostgresType[], isNewRecord = false, @@ -59,7 +58,7 @@ const Column: FC = ({ onEditRelation = () => {}, onUpdateColumn = () => {}, onRemoveColumn = () => {}, -}) => { +}: Props) => { const suggestions: Suggestion[] = typeExpressionSuggestions?.[column.format] ?? [] const settingsCount = [ @@ -150,7 +149,11 @@ const Column: FC = ({ className="table-editor-column-type lg:gap-0 " disabled={column.foreignKey !== undefined} onOptionSelect={(format: string) => { - onUpdateColumn({ format, defaultValue: null }) + let defaultValue = null + if (format === 'uuid') { + defaultValue = 'gen_random_uuid()' + } + onUpdateColumn({ format, defaultValue }) }} />
From 408fb86e40557dc9260e0eb27ecb680598b3c2a6 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Fri, 21 Jul 2023 18:32:01 +1000 Subject: [PATCH 22/80] fix projectlinker ts --- .../components/interfaces/Integrations/ProjectLinker.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/studio/components/interfaces/Integrations/ProjectLinker.tsx b/studio/components/interfaces/Integrations/ProjectLinker.tsx index ef9841015d7..1af35704f6b 100644 --- a/studio/components/interfaces/Integrations/ProjectLinker.tsx +++ b/studio/components/interfaces/Integrations/ProjectLinker.tsx @@ -83,10 +83,9 @@ const ProjectLinker = ({ function onCreateConnections() { const projectDetails = foreignProjects.filter((x) => x.id === vercelProjectId)[0] - if (!organizationIntegrationId) { - console.error('No integration ID set') - return - } + if (!organizationIntegrationId) return console.error('No integration ID set') + if (!vercelProjectId) return console.error('No Vercel project ID set') + if (!supabaseProjectRef) return console.error('No Supabase project ref set') if (setLoading) setLoading(true) From 889eb168c021dba5be9fc4c72d1cd29eabac88bf Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 21 Jul 2023 10:35:34 +0200 Subject: [PATCH 23/80] feat: initial commit --- apps/docs/pages/guides/auth/auth-smtp.mdx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 apps/docs/pages/guides/auth/auth-smtp.mdx diff --git a/apps/docs/pages/guides/auth/auth-smtp.mdx b/apps/docs/pages/guides/auth/auth-smtp.mdx new file mode 100644 index 00000000000..75507703897 --- /dev/null +++ b/apps/docs/pages/guides/auth/auth-smtp.mdx @@ -0,0 +1,19 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'auth-smtp', + title: 'Configure a Custom SMTP', + description: 'Configure SMTP for .', +} + +## Overview + +- Benefits of setting up SMTP + + + + +## How to Set up SMTP + + +### SMTP Providers available From fc65ca9ce3c3488e41eec884cf8a7a32a54cd1f0 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 21 Jul 2023 13:36:17 +0200 Subject: [PATCH 24/80] fix: add section on configuring custom smtp --- .../NavigationMenu.constants.ts | 1 + apps/docs/pages/guides/auth/auth-smtp.mdx | 21 +++++++++++++++--- .../pages/guides/platform/going-into-prod.mdx | 2 +- .../docs/public/img/guides/auth-smtp/smtp.png | Bin 0 -> 199772 bytes 4 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 apps/docs/public/img/guides/auth-smtp/smtp.png diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 0cfe4ed1208..c1162c3d428 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -474,6 +474,7 @@ export const auth = { url: undefined, items: [ { name: 'Enable Captcha Protection', url: '/guides/auth/auth-captcha' }, + { name: 'Configuring Custom SMTP', url: '/guides/auth/auth-smtp' }, { name: 'Managing User Data', url: '/guides/auth/managing-user-data' }, { name: 'Multi-Factor Authentication', url: '/guides/auth/auth-mfa' }, { name: 'Row Level Security', url: '/guides/auth/row-level-security' }, diff --git a/apps/docs/pages/guides/auth/auth-smtp.mdx b/apps/docs/pages/guides/auth/auth-smtp.mdx index 75507703897..dd8b361016b 100644 --- a/apps/docs/pages/guides/auth/auth-smtp.mdx +++ b/apps/docs/pages/guides/auth/auth-smtp.mdx @@ -3,17 +3,32 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { id: 'auth-smtp', title: 'Configure a Custom SMTP', - description: 'Configure SMTP for .', + description: 'Moving towards production: Configuring a custom SMTP provider', } ## Overview -- Benefits of setting up SMTP - +At present, you can trial the Supabase platform by sending up to four emails per hour via the built service. As you progress toward production, you may find yourself wanting for a custom SMTP service in order to increase your limits. A custom SMTP server will allow you to set your own cap on the number of emails sent per hour. +Beyond rate limits, an SMTP server might also help with: +- Deliverability and Reputation Management +- Scalability +- Analytics and Tracking +- Compliance and Anti Spam measures ## How to Set up SMTP +Head over to the [Auth Settings Page](https://supabase.com/dashboard/project/_/settings/auth) and hit "Enable Custom SMTP" under the SMTP Provider section. + +Fill in the fields in the [relevant section](/docs/img/guides/auth-smtp/smtp.png). + +## Configuring and Interpreting rate limits + +The rate limit for sending emails is an aggregate number applied across all endpoints which send emails across all users using the service. For example, if there are 50 users on the platform and the rate limit is `RATE_LIMIT`, the total number of emails that can be sent per hour is `RATE_LIMIT` and not `RATE_LIMIT` * 50. + ### SMTP Providers available +- [Twilio SendGrid](https://docs.sendgrid.com/for-developers/sending-email/integrating-with-the-smtp-api) +- [AWS SES](https://docs.aws.amazon.com/ses/latest/dg/send-email-smtp.html) +- [Resend](https://resend.com/docs/dashboard/emails/introduction) diff --git a/apps/docs/pages/guides/platform/going-into-prod.mdx b/apps/docs/pages/guides/platform/going-into-prod.mdx index c3ac2663e8c..29f09ca748b 100644 --- a/apps/docs/pages/guides/platform/going-into-prod.mdx +++ b/apps/docs/pages/guides/platform/going-into-prod.mdx @@ -40,7 +40,7 @@ After developing your project and deciding it's Production Ready, you should run ## Availability - Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails (see Auth > Settings) - - you can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. + - you can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. You can refer to our [SMTP guide](/docs/guides/auth/auth-smtp) for more details. - The default rate limit for auth emails provided by Supabase is 30 new users per hour, if doing a major public announcement you will likely require more than this. - If your application is on the free plan and is **not** expected to be queried at least once every 7 days, then it may be paused by Supabase to save on server resources. - You can restore paused projects from the Supabase dashboard. diff --git a/apps/docs/public/img/guides/auth-smtp/smtp.png b/apps/docs/public/img/guides/auth-smtp/smtp.png new file mode 100644 index 0000000000000000000000000000000000000000..cbb3a8838a738f01627fd59f0e9eb0b5df6a13e2 GIT binary patch literal 199772 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