├── ios
├── Runner
│ ├── Runner-Bridging-Header.h
│ ├── Assets.xcassets
│ │ ├── LaunchImage.imageset
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ ├── README.md
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ ├── Icon-App-20x20@1x.png
│ │ │ ├── Icon-App-20x20@2x.png
│ │ │ ├── Icon-App-20x20@3x.png
│ │ │ ├── Icon-App-29x29@1x.png
│ │ │ ├── Icon-App-29x29@2x.png
│ │ │ ├── Icon-App-29x29@3x.png
│ │ │ ├── Icon-App-40x40@1x.png
│ │ │ ├── Icon-App-40x40@2x.png
│ │ │ ├── Icon-App-40x40@3x.png
│ │ │ ├── Icon-App-60x60@2x.png
│ │ │ ├── Icon-App-60x60@3x.png
│ │ │ ├── Icon-App-76x76@1x.png
│ │ │ ├── Icon-App-76x76@2x.png
│ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ ├── Icon-App-83.5x83.5@2x.png
│ │ │ └── Contents.json
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ ├── Main.storyboard
│ │ └── LaunchScreen.storyboard
│ └── Info.plist
├── Flutter
│ ├── Debug.xcconfig
│ ├── Release.xcconfig
│ └── AppFrameworkInfo.plist
├── Runner.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ └── project.pbxproj
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── WorkspaceSettings.xcsettings
│ │ └── IDEWorkspaceChecks.plist
├── .gitignore
└── Podfile
├── web
├── favicon.ico
├── icons
│ ├── Icon-192.png
│ └── Icon-512.png
├── manifest.json
└── index.html
├── assets
└── images
│ ├── logo.png
│ ├── logo-dark.png
│ └── noavatar.jpeg
├── analysis_options.yaml
├── android
├── gradle.properties
├── app
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── launcher_icon.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── launcher_icon.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── launcher_icon.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── launcher_icon.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── launcher_icon.png
│ │ │ │ ├── drawable
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable-v21
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── values
│ │ │ │ │ └── styles.xml
│ │ │ │ └── values-night
│ │ │ │ │ └── styles.xml
│ │ │ ├── kotlin
│ │ │ │ └── io
│ │ │ │ │ └── supabase
│ │ │ │ │ └── supabase_demo
│ │ │ │ │ └── MainActivity.kt
│ │ │ └── AndroidManifest.xml
│ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ └── profile
│ │ │ └── AndroidManifest.xml
│ └── build.gradle
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
└── build.gradle
├── lib
├── utils
│ ├── constants.dart
│ └── helpers.dart
├── components
│ ├── auth_required_state.dart
│ └── auth_state.dart
├── configure_web.dart
├── screens
│ ├── splash_screen.dart
│ ├── web_home_screen.dart
│ ├── forgot_password.dart
│ ├── signup_screen.dart
│ ├── change_password.dart
│ ├── signin_screen.dart
│ └── profile_screen.dart
├── configure_nonweb.dart
└── main.dart
├── .metadata
├── .gitignore
├── test
└── widget_test.dart
├── pubspec.yaml
├── README.md
└── pubspec.lock
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/web/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/web/favicon.ico
--------------------------------------------------------------------------------
/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/assets/images/logo.png
--------------------------------------------------------------------------------
/web/icons/Icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/web/icons/Icon-192.png
--------------------------------------------------------------------------------
/web/icons/Icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/web/icons/Icon-512.png
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:lint/analysis_options.yaml
2 |
3 | linter:
4 | rules:
5 | avoid_print: false
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/assets/images/logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/assets/images/logo-dark.png
--------------------------------------------------------------------------------
/assets/images/noavatar.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/assets/images/noavatar.jpeg
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/android/app/src/main/res/mipmap-hdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/android/app/src/main/res/mipmap-mdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamhieu/supabase-flutter-demo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/io/supabase/supabase_demo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package io.supabase.supabase_demo
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
7 |
--------------------------------------------------------------------------------
/lib/utils/constants.dart:
--------------------------------------------------------------------------------
1 | /// TODO: update with your custom SCHEME and HOSTNAME
2 | const myAuthRedirectUri = 'io.supabase.flutterdemo://login-callback';
3 |
4 | /// TODO: Add your SUPABASE_URL / SUPABASE_KEY here
5 | const supabaseUrl = 'SUPABASE_URL';
6 | const supabaseAnnonKey = 'SUPABASE_KEY';
7 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/lib/components/auth_required_state.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:supabase_flutter/supabase_flutter.dart';
3 |
4 | class AuthRequiredState
5 | extends SupabaseAuthRequiredState {
6 | @override
7 | void onUnauthenticated() {
8 | Navigator.pushNamedAndRemoveUntil(context, '/signIn', (route) => false);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/lib/configure_web.dart:
--------------------------------------------------------------------------------
1 | import 'package:supabase_flutter/supabase_flutter.dart';
2 |
3 | import 'utils/constants.dart';
4 |
5 | Future configureApp() async {
6 | // init Supabase singleton
7 | // no localStorage provided, fallback to use hive as default
8 | await Supabase.initialize(
9 | url: supabaseUrl,
10 | anonKey: supabaseAnnonKey,
11 | authCallbackUrlHostname: 'login-callback',
12 | debug: true,
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
4 | def properties = new Properties()
5 |
6 | assert localPropertiesFile.exists()
7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
8 |
9 | def flutterSdkPath = properties.getProperty("flutter.sdk")
10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
12 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/web/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "supabase_demo",
3 | "short_name": "supabase_demo",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "background_color": "#0175C2",
7 | "theme_color": "#0175C2",
8 | "description": "A new Flutter project.",
9 | "orientation": "portrait-primary",
10 | "prefer_related_applications": false,
11 | "icons": [
12 | {
13 | "src": "icons/Icon-192.png",
14 | "sizes": "192x192",
15 | "type": "image/png"
16 | },
17 | {
18 | "src": "icons/Icon-512.png",
19 | "sizes": "512x512",
20 | "type": "image/png"
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.3.50'
3 | repositories {
4 | google()
5 | jcenter()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:4.1.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | jcenter()
18 | }
19 | }
20 |
21 | rootProject.buildDir = '../build'
22 | subprojects {
23 | project.buildDir = "${rootProject.buildDir}/${project.name}"
24 | project.evaluationDependsOn(':app')
25 | }
26 |
27 | task clean(type: Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/ephemeral/
22 | Flutter/app.flx
23 | Flutter/app.zip
24 | Flutter/flutter_assets/
25 | Flutter/flutter_export_environment.sh
26 | ServiceDefinitions.json
27 | Runner/GeneratedPluginRegistrant.*
28 |
29 | # Exceptions to above rules.
30 | !default.mode1v3
31 | !default.mode2v3
32 | !default.pbxuser
33 | !default.perspectivev3
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | .vscode/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .packages
31 | .pub-cache/
32 | .pub/
33 | /build/
34 |
35 | # Web related
36 | lib/generated_plugin_registrant.dart
37 |
38 | # Symbolication related
39 | app.*.symbols
40 |
41 | # Obfuscation related
42 | app.*.map.json
43 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 8.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/components/auth_state.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:supabase/supabase.dart' as supabase;
3 | import 'package:supabase_flutter/supabase_flutter.dart';
4 |
5 | class AuthState extends SupabaseAuthState {
6 | @override
7 | void onUnauthenticated() {
8 | Navigator.pushNamedAndRemoveUntil(context, '/signIn', (route) => false);
9 | }
10 |
11 | @override
12 | void onAuthenticated(supabase.Session session) {
13 | Navigator.pushNamedAndRemoveUntil(context, '/profile', (route) => false);
14 | }
15 |
16 | @override
17 | void onPasswordRecovery(supabase.Session session) {
18 | Navigator.pushNamedAndRemoveUntil(
19 | context, '/profile/changePassword', (route) => false);
20 | }
21 |
22 | @override
23 | void onErrorAuthenticating(String message) {
24 | print('***** onErrorAuthenticating: $message');
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/lib/utils/helpers.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:flutter/foundation.dart';
4 |
5 | import '/utils/constants.dart';
6 |
7 | String? validateEmail(String? value) {
8 | const String pattern =
9 | r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]"
10 | r"{0,253}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]"
11 | r"{0,253}[a-zA-Z0-9])?)*$";
12 | final RegExp regex = RegExp(pattern);
13 | if (value == null || !regex.hasMatch(value)) {
14 | return 'Not a valid email.';
15 | } else {
16 | return null;
17 | }
18 | }
19 |
20 | String? validatePassword(String? value) {
21 | return value == null || value.isEmpty ? 'Invalid password' : null;
22 | }
23 |
24 | String randomString(int length) {
25 | const ch = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz';
26 | final Random r = Random();
27 | return String.fromCharCodes(
28 | Iterable.generate(length, (_) => ch.codeUnitAt(r.nextInt(ch.length))));
29 | }
30 |
31 | String? get authRedirectUri {
32 | if (kIsWeb) {
33 | return null;
34 | } else {
35 | return myAuthRedirectUri;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/widget_test.dart:
--------------------------------------------------------------------------------
1 | // This is a basic Flutter widget test.
2 | //
3 | // To perform an interaction with a widget in your test, use the WidgetTester
4 | // utility that Flutter provides. For example, you can send tap and scroll
5 | // gestures. You can also use WidgetTester to find child widgets in the widget
6 | // tree, read text, and verify that the values of widget properties are correct.
7 |
8 | import 'package:flutter/material.dart';
9 | import 'package:flutter_test/flutter_test.dart';
10 |
11 | import 'package:supabase_demo/main.dart';
12 |
13 | void main() {
14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async {
15 | // Build our app and trigger a frame.
16 | await tester.pumpWidget(MyApp());
17 |
18 | // Verify that our counter starts at 0.
19 | expect(find.text('0'), findsOneWidget);
20 | expect(find.text('1'), findsNothing);
21 |
22 | // Tap the '+' icon and trigger a frame.
23 | await tester.tap(find.byIcon(Icons.add));
24 | await tester.pump();
25 |
26 | // Verify that our counter has incremented.
27 | expect(find.text('0'), findsNothing);
28 | expect(find.text('1'), findsOneWidget);
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/lib/screens/splash_screen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/material.dart';
5 |
6 | import '/components/auth_state.dart';
7 |
8 | class SplashScreen extends StatefulWidget {
9 | @override
10 | SplashScreenState createState() => SplashScreenState();
11 | }
12 |
13 | class SplashScreenState extends AuthState
14 | with SingleTickerProviderStateMixin {
15 | Timer? recoverSessionTimer;
16 |
17 | @override
18 | void initState() {
19 | super.initState();
20 |
21 | /// a timer to slow down session restore
22 | /// If not user can't really see the splash screen
23 | const _duration = Duration(seconds: 1);
24 | recoverSessionTimer = Timer(_duration, () {
25 | recoverSupabaseSession();
26 | });
27 | }
28 |
29 | /// on received auth deeplink, we should cancel recoverSessionTimer
30 | /// and wait for auth deep link handling result
31 | @override
32 | void onReceivedAuthDeeplink(Uri uri) {
33 | if (recoverSessionTimer != null) {
34 | recoverSessionTimer!.cancel();
35 | }
36 | }
37 |
38 | @override
39 | Widget build(BuildContext context) {
40 | return Scaffold(
41 | body: Center(
42 | child: SizedBox(
43 | height: 50.0,
44 | child: Image.asset(
45 | "assets/images/logo-dark.png",
46 | ),
47 | ),
48 | ),
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lib/configure_nonweb.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_secure_storage/flutter_secure_storage.dart';
2 | import 'package:supabase_flutter/supabase_flutter.dart';
3 |
4 | import 'utils/constants.dart';
5 |
6 | Future configureApp() async {
7 | // init Supabase singleton
8 | await Supabase.initialize(
9 | url: supabaseUrl,
10 | anonKey: supabaseAnnonKey,
11 | authCallbackUrlHostname: 'login-callback',
12 | debug: true,
13 | localStorage: SecureLocalStorage(),
14 | );
15 | }
16 |
17 | // user flutter_secure_storage to persist user session
18 | class SecureLocalStorage extends LocalStorage {
19 | SecureLocalStorage()
20 | : super(
21 | initialize: () async {},
22 | hasAccessToken: () {
23 | const storage = FlutterSecureStorage();
24 | return storage.containsKey(key: supabasePersistSessionKey);
25 | },
26 | accessToken: () {
27 | const storage = FlutterSecureStorage();
28 | return storage.read(key: supabasePersistSessionKey);
29 | },
30 | removePersistedSession: () {
31 | const storage = FlutterSecureStorage();
32 | return storage.delete(key: supabasePersistSessionKey);
33 | },
34 | persistSession: (String value) {
35 | const storage = FlutterSecureStorage();
36 | return storage.write(key: supabasePersistSessionKey, value: value);
37 | },
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | use_frameworks!
32 | use_modular_headers!
33 |
34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
35 | end
36 |
37 | post_install do |installer|
38 | installer.pods_project.targets.each do |target|
39 | flutter_additional_ios_build_settings(target)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import 'configure_nonweb.dart' if (dart.library.html) 'configure_web.dart';
5 | import 'screens/change_password.dart';
6 | import 'screens/forgot_password.dart';
7 | import 'screens/profile_screen.dart';
8 | import 'screens/signin_screen.dart';
9 | import 'screens/signup_screen.dart';
10 | import 'screens/splash_screen.dart';
11 | import 'screens/web_home_screen.dart';
12 |
13 | Future main() async {
14 | WidgetsFlutterBinding.ensureInitialized();
15 | await configureApp();
16 | runApp(MyApp());
17 | }
18 |
19 | class MyApp extends StatelessWidget {
20 | // This widget is the root of your application.
21 | @override
22 | Widget build(BuildContext context) {
23 | return MaterialApp(
24 | title: 'Supabase Demo',
25 | theme: ThemeData.dark(),
26 | initialRoute: '/',
27 | routes: {
28 | '/signIn': (_) => SignInScreen(),
29 | '/signUp': (_) => SignUpScreen(),
30 | '/forgotPassword': (_) => ForgotPasswordScreen(),
31 | '/profile': (_) => ProfileScreen(),
32 | '/profile/changePassword': (_) => ChangePasswordScreen(),
33 | },
34 | onGenerateRoute: generateRoute,
35 | );
36 | }
37 | }
38 |
39 | Route generateRoute(RouteSettings settings) {
40 | switch (settings.name) {
41 | default:
42 | return MaterialPageRoute(
43 | builder: (_) => kIsWeb ? WebHomeScreen() : SplashScreen());
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/screens/web_home_screen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:supabase_flutter/supabase_flutter.dart';
6 |
7 | import '/components/auth_state.dart';
8 |
9 | class WebHomeScreen extends StatefulWidget {
10 | @override
11 | _WebHomeScreenState createState() => _WebHomeScreenState();
12 | }
13 |
14 | class _WebHomeScreenState extends AuthState
15 | with SingleTickerProviderStateMixin {
16 | @override
17 | void initState() {
18 | super.initState();
19 |
20 | final uriParameters = SupabaseAuth.instance.parseUriParameters(Uri.base);
21 | if (uriParameters.containsKey('access_token') &&
22 | uriParameters.containsKey('refresh_token') &&
23 | uriParameters.containsKey('expires_in')) {
24 | /// Uri.base is a auth redirect link
25 | /// Call recoverSessionFromUrl to continue
26 | recoverSessionFromUrl(Uri.base);
27 | }
28 | }
29 |
30 | Future onSignIn() async {
31 | final hasAccessToken = await SupabaseAuth.instance.hasAccessToken;
32 | final String route = hasAccessToken ? '/profile' : '/signIn';
33 |
34 | stopAuthObserver();
35 | Navigator.pushNamed(context, route).then((_) => startAuthObserver());
36 | }
37 |
38 | @override
39 | Widget build(BuildContext context) {
40 | return Scaffold(
41 | appBar: AppBar(
42 | title: const Text('Flutter user management'),
43 | ),
44 | body: Center(
45 | child: SizedBox(
46 | height: 50.0,
47 | child: ElevatedButton(
48 | onPressed: onSignIn,
49 | child: const Text('Sign in'),
50 | ),
51 | ),
52 | ),
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 |
28 | android {
29 | compileSdkVersion 30
30 |
31 | sourceSets {
32 | main.java.srcDirs += 'src/main/kotlin'
33 | }
34 |
35 | defaultConfig {
36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
37 | applicationId "io.supabase.supabase_demo"
38 | minSdkVersion 18
39 | targetSdkVersion 30
40 | versionCode flutterVersionCode.toInteger()
41 | versionName flutterVersionName
42 | }
43 |
44 | buildTypes {
45 | release {
46 | // TODO: Add your own signing config for the release build.
47 | // Signing with the debug keys for now, so `flutter run --release` works.
48 | signingConfig signingConfigs.debug
49 | }
50 | }
51 | }
52 |
53 | flutter {
54 | source '../..'
55 | }
56 |
57 | dependencies {
58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
59 | }
60 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | Supabase Demo
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | $(FLUTTER_BUILD_NAME)
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UIViewControllerBasedStatusBarAppearance
43 |
44 | CFBundleURLTypes
45 |
46 |
47 | CFBundleTypeRole
48 | Editor
49 | CFBundleURLSchemes
50 |
51 |
52 | io.supabase.flutterdemo
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: supabase_demo
2 | description: Supabase flutter user management. users can sign up with a magic link and then update their account with public profile information, including a profile image.
3 |
4 | # The following line prevents the package from being accidentally published to
5 | # pub.dev using `pub publish`. This is preferred for private packages.
6 | publish_to: "none" # Remove this line if you wish to publish to pub.dev
7 |
8 | # The following defines the version and build number for your application.
9 | # A version number is three numbers separated by dots, like 1.2.43
10 | # followed by an optional build number separated by a +.
11 | # Both the version and the builder number may be overridden in flutter
12 | # build by specifying --build-name and --build-number, respectively.
13 | # In Android, build-name is used as versionName while build-number used as versionCode.
14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning
15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
16 | # Read more about iOS versioning at
17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
18 | version: 1.0.0+1
19 |
20 | environment:
21 | sdk: ">=2.12.0 <3.0.0"
22 |
23 | dependencies:
24 | cupertino_icons: ^1.0.3
25 | flutter:
26 | sdk: flutter
27 | flutter_secure_storage: ^4.2.0
28 | image_picker: ^0.8.2
29 | rounded_loading_button: ^2.0.5
30 | supabase: ^0.2.12
31 | supabase_flutter: ^0.2.10
32 |
33 | dev_dependencies:
34 | flutter_launcher_icons: ^0.9.0
35 | flutter_test:
36 | sdk: flutter
37 | lint: ^1.5.3
38 |
39 | flutter_icons:
40 | android: "launcher_icon"
41 | ios: true
42 | image_path: "assets/images/logo.png"
43 |
44 | # For information on the generic Dart part of this file, see the
45 | # following page: https://dart.dev/tools/pub/pubspec
46 |
47 | # The following section is specific to Flutter.
48 | flutter:
49 | # The following line ensures that the Material Icons font is
50 | # included with your application, so that you can use the icons in
51 | # the material Icons class.
52 | uses-material-design: true
53 |
54 | # To add assets to your application, add an assets section, like this:
55 | assets:
56 | - assets/images/
57 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
8 |
15 |
19 |
23 |
28 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
48 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/lib/screens/forgot_password.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:rounded_loading_button/rounded_loading_button.dart';
3 | import 'package:supabase/supabase.dart' as supabase;
4 | import 'package:supabase_flutter/supabase_flutter.dart';
5 |
6 | import '/components/auth_state.dart';
7 | import '/utils/helpers.dart';
8 |
9 | class ForgotPasswordScreen extends StatefulWidget {
10 | @override
11 | _ForgotPasswordState createState() => _ForgotPasswordState();
12 | }
13 |
14 | class _ForgotPasswordState extends AuthState {
15 | final formKey = GlobalKey();
16 | final scaffoldKey = GlobalKey();
17 |
18 | final RoundedLoadingButtonController _btnController =
19 | RoundedLoadingButtonController();
20 |
21 | String _email = '';
22 |
23 | Future _onPasswordRecoverPress(BuildContext context) async {
24 | final form = formKey.currentState;
25 |
26 | if (form != null && form.validate()) {
27 | form.save();
28 | FocusScope.of(context).unfocus();
29 |
30 | final response = await Supabase.instance.client.auth.api
31 | .resetPasswordForEmail(_email,
32 | options: supabase.AuthOptions(redirectTo: authRedirectUri));
33 | if (response.error != null) {
34 | showMessage('Password recovery failed: ${response.error!.message}');
35 | _btnController.reset();
36 | } else {
37 | showMessage('Please check your email for further instructions.');
38 | _btnController.success();
39 | }
40 | }
41 | }
42 |
43 | void showMessage(String message) {
44 | final snackbar = SnackBar(content: Text(message));
45 | ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(snackbar);
46 | }
47 |
48 | @override
49 | Widget build(BuildContext context) {
50 | return Scaffold(
51 | key: scaffoldKey,
52 | appBar: AppBar(
53 | title: const Text('Forgot password'),
54 | ),
55 | body: Padding(
56 | padding: const EdgeInsets.all(15.0),
57 | child: Form(
58 | key: formKey,
59 | child: Column(
60 | children: [
61 | const SizedBox(height: 25.0),
62 | TextFormField(
63 | onSaved: (value) => _email = value ?? '',
64 | validator: (val) => validateEmail(val),
65 | keyboardType: TextInputType.emailAddress,
66 | decoration: const InputDecoration(
67 | hintText: 'Enter your email address',
68 | ),
69 | ),
70 | const SizedBox(height: 35.0),
71 | RoundedLoadingButton(
72 | color: Colors.green,
73 | controller: _btnController,
74 | onPressed: () {
75 | _onPasswordRecoverPress(context);
76 | },
77 | child: const Text(
78 | 'Send reset password instructions',
79 | style: TextStyle(fontSize: 16, color: Colors.white),
80 | ),
81 | )
82 | ],
83 | ),
84 | ),
85 | ),
86 | );
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/lib/screens/signup_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:rounded_loading_button/rounded_loading_button.dart';
3 | import 'package:supabase/supabase.dart' as supabase;
4 | import 'package:supabase_flutter/supabase_flutter.dart';
5 |
6 | import '/components/auth_state.dart';
7 | import '/utils/helpers.dart';
8 |
9 | class SignUpScreen extends StatefulWidget {
10 | @override
11 | _SignUpState createState() => _SignUpState();
12 | }
13 |
14 | class _SignUpState extends AuthState {
15 | final formKey = GlobalKey();
16 | final scaffoldKey = GlobalKey();
17 |
18 | final RoundedLoadingButtonController _btnController =
19 | RoundedLoadingButtonController();
20 |
21 | String _email = '';
22 | String _password = '';
23 |
24 | Future _onSignUpPress(BuildContext context) async {
25 | final form = formKey.currentState;
26 |
27 | if (form != null && form.validate()) {
28 | form.save();
29 | FocusScope.of(context).unfocus();
30 |
31 | final response = await Supabase.instance.client.auth.signUp(
32 | _email, _password,
33 | options: supabase.AuthOptions(redirectTo: authRedirectUri));
34 | if (response.error != null) {
35 | showMessage('Sign up failed: ${response.error!.message}');
36 | _btnController.reset();
37 | } else if (response.data == null && response.user == null) {
38 | showMessage(
39 | "Please check your email and follow the instructions to verify your email address.");
40 | _btnController.success();
41 | } else {
42 | Navigator.pushNamedAndRemoveUntil(
43 | context,
44 | '/profile',
45 | (route) => false,
46 | );
47 | }
48 | }
49 | }
50 |
51 | void showMessage(String message) {
52 | final snackbar = SnackBar(content: Text(message));
53 | ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(snackbar);
54 | }
55 |
56 | @override
57 | Widget build(BuildContext context) {
58 | return Scaffold(
59 | key: scaffoldKey,
60 | appBar: AppBar(
61 | title: const Text('Sign up'),
62 | ),
63 | body: Padding(
64 | padding: const EdgeInsets.all(15.0),
65 | child: Form(
66 | key: formKey,
67 | child: Column(
68 | children: [
69 | const SizedBox(height: 15.0),
70 | TextFormField(
71 | onSaved: (value) => _email = value ?? '',
72 | validator: (val) => validateEmail(val),
73 | keyboardType: TextInputType.emailAddress,
74 | decoration: const InputDecoration(
75 | hintText: 'Enter your email address',
76 | ),
77 | ),
78 | const SizedBox(height: 15.0),
79 | TextFormField(
80 | onSaved: (value) => _password = value ?? '',
81 | validator: (val) => validatePassword(val),
82 | obscureText: true,
83 | decoration: const InputDecoration(
84 | hintText: 'Password',
85 | ),
86 | ),
87 | const SizedBox(height: 15.0),
88 | RoundedLoadingButton(
89 | color: Colors.green,
90 | controller: _btnController,
91 | onPressed: () {
92 | _onSignUpPress(context);
93 | },
94 | child: const Text(
95 | 'Sign up',
96 | style: TextStyle(fontSize: 20, color: Colors.white),
97 | ),
98 | )
99 | ],
100 | ),
101 | ),
102 | ),
103 | );
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/lib/screens/change_password.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:rounded_loading_button/rounded_loading_button.dart';
3 | import 'package:supabase/supabase.dart';
4 | import 'package:supabase_flutter/supabase_flutter.dart';
5 |
6 | import '/components/auth_required_state.dart';
7 | import '/utils/helpers.dart';
8 |
9 | class ChangePasswordScreen extends StatefulWidget {
10 | @override
11 | _ChangePasswordState createState() => _ChangePasswordState();
12 | }
13 |
14 | class _ChangePasswordState extends AuthRequiredState {
15 | final formKey = GlobalKey();
16 | final scaffoldKey = GlobalKey();
17 |
18 | final TextEditingController _passwordField = TextEditingController();
19 |
20 | final RoundedLoadingButtonController _btnController =
21 | RoundedLoadingButtonController();
22 |
23 | String _password = '';
24 |
25 | Future _onPasswordChangePress(BuildContext context) async {
26 | try {
27 | final form = formKey.currentState;
28 |
29 | if (form != null && form.validate()) {
30 | form.save();
31 | FocusScope.of(context).unfocus();
32 |
33 | final userAttributes = UserAttributes(password: _password);
34 | final response =
35 | await Supabase.instance.client.auth.update(userAttributes);
36 | if (response.error != null) {
37 | throw 'Password change failed: ${response.error!.message}';
38 | }
39 |
40 | showMessage('Password updated');
41 | if (Navigator.canPop(context)) {
42 | _btnController.success();
43 | } else {
44 | Navigator.pushNamedAndRemoveUntil(
45 | context,
46 | '/profile',
47 | (route) => false,
48 | );
49 | }
50 | }
51 | } catch (e) {
52 | showMessage(e.toString());
53 | } finally {
54 | _btnController.reset();
55 | }
56 | }
57 |
58 | void showMessage(String message) {
59 | final snackbar = SnackBar(content: Text(message));
60 | ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(snackbar);
61 | }
62 |
63 | @override
64 | Widget build(BuildContext context) {
65 | return Scaffold(
66 | key: scaffoldKey,
67 | appBar: AppBar(
68 | title: const Text('Change password'),
69 | ),
70 | body: Padding(
71 | padding: const EdgeInsets.all(15.0),
72 | child: Form(
73 | key: formKey,
74 | child: Column(
75 | children: [
76 | const SizedBox(height: 25.0),
77 | TextFormField(
78 | onSaved: (value) => _password = value ?? '',
79 | validator: (val) => validatePassword(val),
80 | controller: _passwordField,
81 | obscureText: true,
82 | decoration: const InputDecoration(
83 | hintText: 'Password',
84 | ),
85 | ),
86 | const SizedBox(height: 15.0),
87 | TextFormField(
88 | validator: (val) =>
89 | val != _passwordField.text ? 'Not Match' : null,
90 | obscureText: true,
91 | decoration: const InputDecoration(
92 | hintText: 'Confirm password',
93 | ),
94 | ),
95 | const SizedBox(height: 15.0),
96 | RoundedLoadingButton(
97 | color: Colors.green,
98 | controller: _btnController,
99 | onPressed: () {
100 | _onPasswordChangePress(context);
101 | },
102 | child: const Text(
103 | 'Save',
104 | style: TextStyle(fontSize: 16, color: Colors.white),
105 | ),
106 | ),
107 | ],
108 | ),
109 | ),
110 | ),
111 | );
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | supabase_demo
27 |
28 |
29 |
30 |
33 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Supabase Flutter User Management
2 |
3 | This example will set you up for a very common situation: users can sign up with a magic link and then update their account with public profile information, including a profile image.
4 |
5 | ## Technologies used
6 |
7 | - Frontend:
8 | - [Flutter SDK](https://flutter.dev/) - Google's UI toolkit for crafting beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.
9 | - [supabase_flutter](https://pub.dev/packages/supabase_flutter) for user management and image storage uploading.
10 | - Backend:
11 | - [app.supabase.io](https://app.supabase.io/): hosted Postgres database with restful API for usage with supabase_flutter.
12 |
13 | ## Build from scratch
14 |
15 | ### 1. Create new project
16 |
17 | Sign up to Supabase - [https://app.supabase.io](https://app.supabase.io) and create a new project. Wait for your database to start.
18 |
19 | ### 2. Run "User Management" Quickstart
20 |
21 | Once your database has started, run the "User Management Starter" quickstart. Inside of your project, enter the `SQL editor` tab and scroll down until you see `User Management Starter: Set up a Public Profiles table which you can access with your API`.
22 |
23 | ### 3. Get the URL and Key
24 |
25 | Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step.
26 |
27 | The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token. This enables row level security for your data. Read more about this [below](#postgres-row-level-security).
28 |
29 | 
30 |
31 | **_NOTE_**: The `service_role` key has full access to your data, bypassing any security policies. These keys have to be kept secret and are meant to be used in server environments and never on a client or browser.
32 |
33 | ### 4. Setup deeplink redirect urls
34 |
35 | Go to the Authentication Settings page (the user icon). Enter the flutter app redirect url below into `Additional Redirect URLs` field. Then click Save.
36 |
37 | > io.supabase.flutterdemo://login-callback
38 |
39 | 
40 |
41 | ### 5. Setup Github 3rd party logins
42 |
43 | Follow the guide https://supabase.io/docs/guides/auth#third-party-logins
44 |
45 | ### 6. Run the flutter application
46 |
47 | - Go to `lib/utils/constants.dart` file
48 | - Update `SUPABASE_URL` and `SUPABASE_ANNON_KEY` with your URL and Key
49 | - Run the application: `flutter run`
50 |
51 | ## Supabase details
52 |
53 | ### Postgres Row level security
54 |
55 | This project uses very high-level Authorization using Postgres' Role Level Security.
56 | When you start a Postgres database on Supabase, we populate it with an `auth` schema, and some helper functions.
57 | When a user logs in, they are issued a JWT with the role `authenticated` and thier UUID.
58 | We can use these details to provide fine-grained control over what each user can and cannot do.
59 |
60 | This is a trimmed-down schema, with the policies:
61 |
62 | ```sql
63 | -- Create a table for Public Profiles
64 | create table profiles (
65 | id uuid references auth.users not null,
66 | updated_at timestamp with time zone,
67 | username text unique,
68 | avatar_url text,
69 | website text,
70 |
71 | primary key (id),
72 | unique(username),
73 | constraint username_length check (char_length(username) >= 3)
74 | );
75 |
76 | alter table profiles enable row level security;
77 |
78 | create policy "Public profiles are viewable by everyone."
79 | on profiles for select
80 | using ( true );
81 |
82 | create policy "Users can insert their own profile."
83 | on profiles for insert
84 | with check ( auth.uid() = id );
85 |
86 | create policy "Users can update own profile."
87 | on profiles for update
88 | using ( auth.uid() = id );
89 |
90 | -- Set up Realtime!
91 | begin;
92 | drop publication if exists supabase_realtime;
93 | create publication supabase_realtime;
94 | commit;
95 | alter publication supabase_realtime add table profiles;
96 |
97 | -- Set up Storage!
98 | insert into storage.buckets (id, name)
99 | values ('avatars', 'avatars');
100 |
101 | create policy "Avatar images are publicly accessible."
102 | on storage.objects for select
103 | using ( bucket_id = 'avatars' );
104 |
105 | create policy "Anyone can upload an avatar."
106 | on storage.objects for insert
107 | with check ( bucket_id = 'avatars' );
108 |
109 | create policy "Anyone can update an avatar."
110 | on storage.objects for update
111 | with check ( bucket_id = 'avatars' );
112 | ```
113 |
114 | ## Authors
115 |
116 | - [Supabase](https://supabase.io)
117 |
118 | Supabase is open source. We'd love for you to follow along and get involved at https://github.com/supabase/supabase
119 |
--------------------------------------------------------------------------------
/lib/screens/signin_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:rounded_loading_button/rounded_loading_button.dart';
3 | import 'package:supabase/supabase.dart' as supabase;
4 | import 'package:supabase_flutter/supabase_flutter.dart';
5 |
6 | import '/components/auth_state.dart';
7 | import '/utils/helpers.dart';
8 |
9 | class SignInScreen extends StatefulWidget {
10 | @override
11 | _SignInState createState() => _SignInState();
12 | }
13 |
14 | class _SignInState extends AuthState {
15 | final formKey = GlobalKey();
16 | final scaffoldKey = GlobalKey();
17 |
18 | final RoundedLoadingButtonController _signInEmailController =
19 | RoundedLoadingButtonController();
20 | final RoundedLoadingButtonController _magicLinkController =
21 | RoundedLoadingButtonController();
22 | final RoundedLoadingButtonController _githubSignInController =
23 | RoundedLoadingButtonController();
24 |
25 | String _email = '';
26 | String _password = '';
27 |
28 | @override
29 | void onErrorAuthenticating(String message) {
30 | showMessage(message);
31 | _githubSignInController.reset();
32 | }
33 |
34 | Future _onSignInPress(BuildContext context) async {
35 | final form = formKey.currentState;
36 |
37 | if (form != null && form.validate()) {
38 | form.save();
39 | FocusScope.of(context).unfocus();
40 |
41 | final response = await Supabase.instance.client.auth
42 | .signIn(email: _email, password: _password);
43 | if (response.error != null) {
44 | showMessage(response.error!.message);
45 | _signInEmailController.reset();
46 | } else {
47 | Navigator.pushNamedAndRemoveUntil(
48 | context,
49 | '/profile',
50 | (route) => false,
51 | );
52 | }
53 | } else {
54 | _signInEmailController.reset();
55 | }
56 | }
57 |
58 | Future _onMagicLinkPress(BuildContext context) async {
59 | final form = formKey.currentState;
60 |
61 | if (form != null && form.validate()) {
62 | form.save();
63 | FocusScope.of(context).unfocus();
64 |
65 | final response = await Supabase.instance.client.auth.signIn(
66 | email: _email,
67 | options: supabase.AuthOptions(
68 | redirectTo: authRedirectUri,
69 | ),
70 | );
71 | if (response.error != null) {
72 | showMessage(response.error!.message);
73 | _magicLinkController.reset();
74 | } else {
75 | showMessage('Check your email for the login link!');
76 | }
77 | } else {
78 | _magicLinkController.reset();
79 | }
80 | }
81 |
82 | Future _githubSigninPressed(BuildContext context) async {
83 | FocusScope.of(context).unfocus();
84 |
85 | Supabase.instance.client.auth.signInWithProvider(
86 | supabase.Provider.github,
87 | options: supabase.AuthOptions(redirectTo: authRedirectUri),
88 | );
89 | }
90 |
91 | void showMessage(String message) {
92 | final snackbar = SnackBar(content: Text(message));
93 | ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(snackbar);
94 | }
95 |
96 | @override
97 | Widget build(BuildContext context) {
98 | return Scaffold(
99 | key: scaffoldKey,
100 | resizeToAvoidBottomInset: false,
101 | appBar: AppBar(
102 | title: const Text('Sign in'),
103 | ),
104 | body: Padding(
105 | padding: const EdgeInsets.all(15.0),
106 | child: Form(
107 | key: formKey,
108 | child: Column(
109 | children: [
110 | const SizedBox(height: 25.0),
111 | TextFormField(
112 | onSaved: (value) => _email = value ?? '',
113 | validator: (val) => validateEmail(val),
114 | keyboardType: TextInputType.emailAddress,
115 | decoration: const InputDecoration(
116 | hintText: 'Enter your email address',
117 | ),
118 | ),
119 | const SizedBox(height: 15.0),
120 | TextFormField(
121 | onSaved: (value) => _password = value ?? '',
122 | obscureText: true,
123 | decoration: const InputDecoration(
124 | hintText: 'Password',
125 | ),
126 | ),
127 | const SizedBox(height: 15.0),
128 | RoundedLoadingButton(
129 | color: Colors.green,
130 | controller: _signInEmailController,
131 | onPressed: () {
132 | _onSignInPress(context);
133 | },
134 | child: const Text(
135 | 'Sign in',
136 | style: TextStyle(fontSize: 20, color: Colors.white),
137 | ),
138 | ),
139 | const SizedBox(height: 15.0),
140 | RoundedLoadingButton(
141 | color: Colors.green,
142 | controller: _magicLinkController,
143 | onPressed: () {
144 | _onMagicLinkPress(context);
145 | },
146 | child: const Text(
147 | 'Send magic link',
148 | style: TextStyle(fontSize: 20, color: Colors.white),
149 | ),
150 | ),
151 | const SizedBox(height: 15.0),
152 | RoundedLoadingButton(
153 | color: Colors.black,
154 | controller: _githubSignInController,
155 | onPressed: () {
156 | _githubSigninPressed(context);
157 | },
158 | child: const Text(
159 | 'Github Login',
160 | style: TextStyle(fontSize: 20, color: Colors.white),
161 | ),
162 | ),
163 | const SizedBox(height: 15.0),
164 | TextButton(
165 | onPressed: () {
166 | stopAuthObserver();
167 | Navigator.pushNamed(context, '/forgotPassword')
168 | .then((_) => startAuthObserver());
169 | },
170 | child: const Text("Forgot your password ?"),
171 | ),
172 | TextButton(
173 | onPressed: () {
174 | stopAuthObserver();
175 | Navigator.pushNamed(context, '/signUp')
176 | .then((_) => startAuthObserver());
177 | },
178 | child: const Text("Don’t have an Account ? Sign up"),
179 | ),
180 | ],
181 | ),
182 | ),
183 | ),
184 | );
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/lib/screens/profile_screen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/foundation.dart';
5 | import 'package:supabase/supabase.dart';
6 | import 'package:flutter/material.dart';
7 | import 'package:rounded_loading_button/rounded_loading_button.dart';
8 | import 'package:image_picker/image_picker.dart';
9 | import 'package:supabase_flutter/supabase_flutter.dart';
10 |
11 | import '/components/auth_required_state.dart';
12 | import '/utils/helpers.dart';
13 |
14 | class ProfileScreen extends StatefulWidget {
15 | @override
16 | _ProfileScreenState createState() => _ProfileScreenState();
17 | }
18 |
19 | class _ProfileScreenState extends AuthRequiredState {
20 | _ProfileScreenState();
21 |
22 | final scaffoldKey = GlobalKey();
23 |
24 | final RoundedLoadingButtonController _signOutBtnController =
25 | RoundedLoadingButtonController();
26 | final RoundedLoadingButtonController _updateProfileBtnController =
27 | RoundedLoadingButtonController();
28 |
29 | final _picker = ImagePicker();
30 |
31 | User? user;
32 | bool loadingProfile = true;
33 | String _appBarTitle = '';
34 | String username = '';
35 | String website = '';
36 | String avatarUrl = '';
37 | String avatarKey = '';
38 |
39 | @override
40 | void onAuthenticated(Session session) {
41 | final _user = session.user;
42 | if (_user != null) {
43 | setState(() {
44 | _appBarTitle = 'Welcome ${_user.email}';
45 | user = _user;
46 | });
47 | _loadProfile(_user.id);
48 | }
49 | }
50 |
51 | Future _loadProfile(String userId) async {
52 | try {
53 | final response = await Supabase.instance.client
54 | .from('profiles')
55 | .select('username, website, avatar_url, updated_at')
56 | .eq('id', userId)
57 | .maybeSingle()
58 | .execute();
59 | if (response.error != null) {
60 | throw "Load profile failed: ${response.error!.message}";
61 | }
62 |
63 | setState(() {
64 | print(response.data);
65 | username = response.data?['username'] as String? ?? '';
66 | website = response.data?['website'] as String? ?? '';
67 | avatarUrl = response.data?['avatar_url'] as String? ?? '';
68 | final updatedAt = response.data?['updated_at'] as String? ?? '';
69 | avatarKey = '$avatarUrl-$updatedAt';
70 | });
71 | } catch (e) {
72 | showMessage(e.toString());
73 | } finally {
74 | setState(() {
75 | loadingProfile = false;
76 | });
77 | }
78 | }
79 |
80 | Future _onSignOutPress(BuildContext context) async {
81 | await Supabase.instance.client.auth.signOut();
82 | Navigator.pushNamedAndRemoveUntil(context, '/signIn', (route) => false);
83 | }
84 |
85 | Future _updateAvatar(BuildContext context) async {
86 | try {
87 | final pickedFile = await _picker.pickImage(
88 | source: ImageSource.gallery,
89 | maxHeight: 600,
90 | maxWidth: 600,
91 | );
92 | if (pickedFile == null) {
93 | return;
94 | }
95 |
96 | final size = await pickedFile.length();
97 | if (size > 1000000) {
98 | throw "The file is too large. Allowed maximum size is 1 MB.";
99 | }
100 |
101 | final bytes = await pickedFile.readAsBytes();
102 | final fileName = avatarUrl == '' ? '${randomString(15)}.jpg' : avatarUrl;
103 | const fileOptions = FileOptions(upsert: true);
104 | final uploadRes = await Supabase.instance.client.storage
105 | .from('avatars')
106 | .uploadBinary(fileName, bytes, fileOptions: fileOptions);
107 |
108 | if (uploadRes.error != null) {
109 | throw uploadRes.error!.message;
110 | }
111 |
112 | final updatedAt = DateTime.now().toString();
113 | final res = await Supabase.instance.client.from('profiles').upsert({
114 | 'id': user!.id,
115 | 'avatar_url': fileName,
116 | 'updated_at': updatedAt,
117 | }).execute();
118 | if (res.error != null) {
119 | throw res.error!.message;
120 | }
121 |
122 | setState(() {
123 | avatarUrl = fileName;
124 | avatarKey = '$fileName-$updatedAt';
125 | });
126 | showMessage("Avatar updated!");
127 | } catch (e) {
128 | showMessage(e.toString());
129 | }
130 | }
131 |
132 | Future _onUpdateProfilePress(BuildContext context) async {
133 | try {
134 | FocusScope.of(context).unfocus();
135 |
136 | final updates = {
137 | 'id': user?.id,
138 | 'username': username,
139 | 'website': website,
140 | 'updated_at': DateTime.now().toString(),
141 | };
142 |
143 | final response = await Supabase.instance.client
144 | .from('profiles')
145 | .upsert(updates)
146 | .execute();
147 | if (response.error != null) {
148 | throw "Update profile failed: ${response.error!.message}";
149 | }
150 |
151 | showMessage("Profile updated!");
152 | } catch (e) {
153 | showMessage(e.toString());
154 | } finally {
155 | _updateProfileBtnController.reset();
156 | }
157 | }
158 |
159 | void showMessage(String message) {
160 | final snackbar = SnackBar(content: Text(message));
161 | ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(snackbar);
162 | }
163 |
164 | @override
165 | Widget build(BuildContext context) {
166 | if (loadingProfile) {
167 | return Scaffold(
168 | appBar: AppBar(
169 | title: Text(_appBarTitle),
170 | ),
171 | body: SizedBox(
172 | height: MediaQuery.of(context).size.height / 1.3,
173 | child: const Center(
174 | child: CircularProgressIndicator(),
175 | ),
176 | ),
177 | );
178 | } else {
179 | return Scaffold(
180 | key: scaffoldKey,
181 | resizeToAvoidBottomInset: false,
182 | appBar: AppBar(
183 | title: Text(_appBarTitle),
184 | ),
185 | body: Padding(
186 | padding: const EdgeInsets.all(15.0),
187 | child: Column(
188 | children: [
189 | AvatarContainer(
190 | url: avatarUrl,
191 | onUpdatePressed: () => _updateAvatar(context),
192 | key: Key(avatarKey),
193 | ),
194 | TextFormField(
195 | onChanged: (value) => setState(() {
196 | username = value;
197 | }),
198 | initialValue: username,
199 | keyboardType: TextInputType.emailAddress,
200 | decoration: const InputDecoration(
201 | labelText: 'Username',
202 | hintText: '',
203 | ),
204 | ),
205 | TextFormField(
206 | onChanged: (value) => setState(() {
207 | website = value;
208 | }),
209 | initialValue: website,
210 | keyboardType: TextInputType.emailAddress,
211 | decoration: const InputDecoration(
212 | labelText: 'Website',
213 | hintText: '',
214 | ),
215 | ),
216 | const SizedBox(
217 | height: 35.0,
218 | ),
219 | RoundedLoadingButton(
220 | color: Colors.green,
221 | controller: _updateProfileBtnController,
222 | onPressed: () {
223 | _onUpdateProfilePress(context);
224 | },
225 | child: const Text('Update profile',
226 | style: TextStyle(fontSize: 20, color: Colors.white)),
227 | ),
228 | const SizedBox(height: 15.0),
229 | TextButton(
230 | onPressed: () {
231 | stopAuthObserver();
232 | Navigator.pushNamed(context, '/profile/changePassword')
233 | .then((_) => startAuthObserver());
234 | },
235 | child: const Text("Change password"),
236 | ),
237 | const Expanded(child: SizedBox()),
238 | RoundedLoadingButton(
239 | color: Colors.red,
240 | controller: _signOutBtnController,
241 | onPressed: () {
242 | _onSignOutPress(context);
243 | },
244 | child: const Text('Sign out',
245 | style: TextStyle(fontSize: 20, color: Colors.white)),
246 | ),
247 | ],
248 | ),
249 | ),
250 | );
251 | }
252 | }
253 | }
254 |
255 | class AvatarContainer extends StatefulWidget {
256 | final String url;
257 | final void Function() onUpdatePressed;
258 | const AvatarContainer(
259 | {required this.url, required this.onUpdatePressed, Key? key})
260 | : super(key: key);
261 |
262 | @override
263 | _AvatarContainerState createState() => _AvatarContainerState();
264 | }
265 |
266 | class _AvatarContainerState extends State {
267 | _AvatarContainerState();
268 |
269 | bool loadingImage = false;
270 | Uint8List? image;
271 |
272 | @override
273 | void initState() {
274 | super.initState();
275 |
276 | if (widget.url != '') {
277 | downloadImage(widget.url);
278 | }
279 | }
280 |
281 | Future downloadImage(String path) async {
282 | setState(() {
283 | loadingImage = true;
284 | });
285 |
286 | final response =
287 | await Supabase.instance.client.storage.from('avatars').download(path);
288 | if (response.error == null) {
289 | setState(() {
290 | image = response.data;
291 | loadingImage = false;
292 | });
293 | } else {
294 | print(response.error!.message);
295 | setState(() {
296 | loadingImage = false;
297 | });
298 | }
299 | return true;
300 | }
301 |
302 | ImageProvider