docs: Update getSession usage in Angluar ionic tutorial (#43489)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES
This commit is contained in:
Chris Chinchilla authored and GitHub committed 2026-03-09 10:26:54 +01:00
1 parent 579357089b
commit c20cfe832a
61 files changed
+23451 -502

No files matched your search

@@ -9,19 +9,22 @@ description: 'Learn how to use Supabase in your Ionic Angular App.'
<Admonition type="note">
If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/mhartington/supabase-ionic-angular).
If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/ionic-angular-user-management).
</Admonition>
<$Partial path="project_setup.mdx" variables={{ "framework": "ionicangular", "tab": "mobiles" }} />
<$Partial
path="project_setup.mdx"
variables={{ "framework": "ionicangular", "tab": "mobiles" }}
/>
## Building the app
Let's start building the Angular app from scratch.
Start building the Angular app from scratch.
### Initialize an Ionic Angular app
We can use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize
Use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize
an app called `supabase-ionic-angular`:
```bash
@@ -30,368 +33,81 @@ ionic start supabase-ionic-angular blank --type angular
cd supabase-ionic-angular
```
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
Install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npm install @supabase/supabase-js
```
And finally, we want to save the environment variables in the `src/environments/environment.ts` file.
All we need are the API URL and the key that you copied [earlier](#get-api-details).
These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
And finally, save the environment variables in the `src/environments/environment.ts` file.
All you need are the API URL and the key that you copied [earlier](#get-api-details).
These variables will be exposed on the browser, and that's fine as [Row Level Security](/docs/guides/auth#row-level-security) is enabled on the Database.
<$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/environments/environment.ts"
lines={[[1, -1]]}
meta="name=src/environments/environment.ts"
/>
```ts name=environment.ts
export const environment = {
production: false,
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseKey: 'YOUR_SUPABASE_KEY',
}
```
Now that you have the API credentials in place, create a `SupabaseService` with `ionic g s supabase` to initialize the Supabase client and implement functions to communicate with the Supabase API.
</$CodeTabs>
Now that we have the API credentials in place, let's create a `SupabaseService` with `ionic g s supabase` to initialize the Supabase client and implement functions to communicate with the Supabase API.
<$CodeTabs>
```ts name=src/app/supabase.service.ts
import { Injectable } from '@angular/core'
import { LoadingController, ToastController } from '@ionic/angular'
import { AuthChangeEvent, createClient, Session, SupabaseClient } from '@supabase/supabase-js'
import { environment } from '../environments/environment'
export interface Profile {
username: string
website: string
avatar_url: string
}
@Injectable({
providedIn: 'root',
})
export class SupabaseService {
private supabase: SupabaseClient
constructor(
private loadingCtrl: LoadingController,
private toastCtrl: ToastController
) {
this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)
}
get user() {
return this.supabase.auth.getUser().then(({ data }) => data?.user)
}
get session() {
return this.supabase.auth.getSession().then(({ data }) => data?.session)
}
get profile() {
return this.user
.then((user) => user?.id)
.then((id) =>
this.supabase.from('profiles').select(`username, website, avatar_url`).eq('id', id).single()
)
}
authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) {
return this.supabase.auth.onAuthStateChange(callback)
}
signIn(email: string) {
return this.supabase.auth.signInWithOtp({ email })
}
signOut() {
return this.supabase.auth.signOut()
}
async updateProfile(profile: Profile) {
const user = await this.user
const update = {
...profile,
id: user?.id,
updated_at: new Date(),
}
return this.supabase.from('profiles').upsert(update)
}
downLoadImage(path: string) {
return this.supabase.storage.from('avatars').download(path)
}
uploadAvatar(filePath: string, file: File) {
return this.supabase.storage.from('avatars').upload(filePath, file)
}
async createNotice(message: string) {
const toast = await this.toastCtrl.create({ message, duration: 5000 })
await toast.present()
}
createLoader() {
return this.loadingCtrl.create()
}
}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/supabase.service.ts"
lines={[[1, -1]]}
meta="name=src/app/supabase.service.ts"
/>
### Set up a login route
Let's set up a route to manage logins and signups. We'll use Magic Links so users can sign in with their email without using passwords.
Set up a route to manage logins and signups. Use Magic Links so users can sign in with their email without using passwords.
Create a `LoginPage` with the `ionic g page login` Ionic CLI command.
<Admonition type="tip">
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/login/login.page.ts"
lines={[[1, -1]]}
meta="name=src/app/login/login.page.ts"
/>
This guide will show the template inline, but the example app will have `templateUrl`s
</Admonition>
<$CodeTabs>
```ts name=src/app/login/login.page.ts
import { Component, OnInit } from '@angular/core'
import { SupabaseService } from '../supabase.service'
@Component({
selector: 'app-login',
template: `
<ion-header>
<ion-toolbar>
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<h1>Supabase + Ionic Angular</h1>
<p>Sign in via magic link with your email below</p>
</div>
<ion-list inset="true">
<form (ngSubmit)="handleLogin($event)">
<ion-item>
<ion-label position="stacked">Email</ion-label>
<ion-input [(ngModel)]="email" name="email" autocomplete type="email"></ion-input>
</ion-item>
<div class="ion-text-center">
<ion-button type="submit" fill="clear">Login</ion-button>
</div>
</form>
</ion-list>
</ion-content>
`,
styleUrls: ['./login.page.scss'],
})
export class LoginPage {
email = ''
constructor(private readonly supabase: SupabaseService) {}
async handleLogin(event: any) {
event.preventDefault()
const loader = await this.supabase.createLoader()
await loader.present()
try {
const { error } = await this.supabase.signIn(this.email)
if (error) {
throw error
}
await loader.dismiss()
await this.supabase.createNotice('Check your email for the login link!')
} catch (error: any) {
await loader.dismiss()
await this.supabase.createNotice(error.error_description || error.message)
}
}
}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/login/login.page.html"
lines={[[1, -1]]}
meta="name=src/app/login/login.page.html"
/>
### Account page
After a user is signed in, we can allow them to edit their profile details and manage their account.
After a user is signed in, allow them to edit their profile details and manage their account.
Create an `AccountComponent` with `ionic g page account` Ionic CLI command.
<$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/account/account.page.ts"
lines={[[1, -1]]}
meta="name=src/app/account/account.page.ts"
/>
```ts name=src/app/account.page.ts
import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router'
import { Profile, SupabaseService } from '../supabase.service'
@Component({
selector: 'app-account',
template: `
<ion-header>
<ion-toolbar>
<ion-title>Account</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form>
<ion-item>
<ion-label position="stacked">Email</ion-label>
<ion-input type="email" name="email" [(ngModel)]="email" readonly></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Name</ion-label>
<ion-input type="text" name="username" [(ngModel)]="profile.username"></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Website</ion-label>
<ion-input type="url" name="website" [(ngModel)]="profile.website"></ion-input>
</ion-item>
<div class="ion-text-center">
<ion-button fill="clear" (click)="updateProfile()">Update Profile</ion-button>
</div>
</form>
<div class="ion-text-center">
<ion-button fill="clear" (click)="signOut()">Log Out</ion-button>
</div>
</ion-content>
`,
styleUrls: ['./account.page.scss'],
})
export class AccountPage implements OnInit {
profile: Profile = {
username: '',
avatar_url: '',
website: '',
}
email = ''
constructor(
private readonly supabase: SupabaseService,
private router: Router
) {}
ngOnInit() {
this.getEmail()
this.getProfile()
}
async getEmail() {
this.email = await this.supabase.user.then((user) => user?.email || '')
}
async getProfile() {
try {
const { data: profile, error, status } = await this.supabase.profile
if (error && status !== 406) {
throw error
}
if (profile) {
this.profile = profile
}
} catch (error: any) {
alert(error.message)
}
}
async updateProfile(avatar_url: string = '') {
const loader = await this.supabase.createLoader()
await loader.present()
try {
const { error } = await this.supabase.updateProfile({ ...this.profile, avatar_url })
if (error) {
throw error
}
await loader.dismiss()
await this.supabase.createNotice('Profile updated!')
} catch (error: any) {
await loader.dismiss()
await this.supabase.createNotice(error.message)
}
}
async signOut() {
console.log('testing?')
await this.supabase.signOut()
this.router.navigate(['/'], { replaceUrl: true })
}
}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/account/account.page.html"
lines={[[1, -1]]}
meta="name=src/app/account/account.page.html"
/>
### Launch!
Now that we have all the components in place, let's update `AppComponent`:
Now that you have all the components in place, update `AppComponent`:
<$CodeTabs>
```ts name=src/app/app.component.ts
import { Component } from '@angular/core'
import { Router } from '@angular/router'
import { SupabaseService } from './supabase.service'
@Component({
selector: 'app-root',
template: `
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>
`,
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor(
private supabase: SupabaseService,
private router: Router
) {
this.supabase.authChanges((_, session) => {
console.log(session)
if (session?.user) {
this.router.navigate(['/account'])
}
})
}
}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/app.component.ts"
lines={[[1, -1]]}
meta="name=src/app/app.component.ts"
/>
Then update the `AppRoutingModule`
<$CodeTabs>
```ts name=src/app/app-routing.module.ts"
import { NgModule } from '@angular/core'
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
const routes: Routes = [
{
path: '',
loadChildren: () => import('./login/login.module').then((m) => m.LoginPageModule),
},
{
path: 'account',
loadChildren: () => import('./account/account.module').then((m) => m.AccountPageModule),
},
]
@NgModule({
imports: [
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules,
}),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/app-routing.module.ts"
lines={[[1, -1]]}
meta="name=src/app/app-routing.module.ts"
/>
Once that's done, run this in a terminal window:
@@ -399,7 +115,7 @@ Once that's done, run this in a terminal window:
ionic serve
```
And the browser will automatically open to show the app.
And the browser automatically opens to show the app.
![Supabase Angular](/docs/img/ionic-demos/ionic-angular.png)
@@ -419,178 +135,39 @@ npm install @ionic/pwa-elements @capacitor/camera
[Capacitor](https://capacitorjs.com) is a cross-platform native runtime from Ionic that enables web apps to be deployed through the app store and provides access to native device API.
Ionic PWA elements is a companion package that will polyfill certain browser APIs that provide no user interface with custom Ionic UI.
Ionic PWA elements is a companion package that polyfills certain browser APIs that provide no user interface with custom Ionic UI.
With those packages installed, we can update our `main.ts` to include an additional bootstrapping call for the Ionic PWA Elements.
With those packages installed, update `main.ts` to include an additional bootstrapping call for the Ionic PWA Elements.
<$CodeTabs>
```ts name=src/main.ts
import { enableProdMode } from '@angular/core'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app/app.module'
import { environment } from './environments/environment'
import { defineCustomElements } from '@ionic/pwa-elements/loader'
defineCustomElements(window)
if (environment.production) {
enableProdMode()
}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.log(err))
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/main.ts"
lines={[[1, -1]]}
meta="name=src/main.ts"
/>
Then create an `AvatarComponent` with this Ionic CLI command:
```bash
ionic g component avatar --module=/src/app/account/account.module.ts --create-module
ionic g component avatar --module=/src/app/account/account.module.ts --create-module
```
<$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/avatar/avatar.component.ts"
lines={[[1, -1]]}
meta="name=src/app/avatar/avatar.component.ts"
/>
```ts name=src/app/avatar.component.ts
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
import { SupabaseService } from '../supabase.service'
import { Camera, CameraResultType } from '@capacitor/camera'
import { addIcons } from 'ionicons'
import { person } from 'ionicons/icons'
@Component({
selector: 'app-avatar',
template: `
<div class="avatar_wrapper" (click)="uploadAvatar()">
<img *ngIf="_avatarUrl; else noAvatar" [src]="_avatarUrl" />
<ng-template #noAvatar>
<ion-icon name="person" class="no-avatar"></ion-icon>
</ng-template>
</div>
`,
style: [
`
:host {
display: block;
margin: auto;
min-height: 150px;
}
:host .avatar_wrapper {
margin: 16px auto 16px;
border-radius: 50%;
overflow: hidden;
height: 150px;
aspect-ratio: 1;
background: var(--ion-color-step-50);
border: thick solid var(--ion-color-step-200);
}
:host .avatar_wrapper:hover {
cursor: pointer;
}
:host .avatar_wrapper ion-icon.no-avatar {
width: 100%;
height: 115%;
}
:host img {
display: block;
object-fit: cover;
width: 100%;
height: 100%;
}
`,
],
})
export class AvatarComponent {
_avatarUrl: SafeResourceUrl | undefined
uploading = false
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/avatar/avatar.component.html"
lines={[[1, -1]]}
meta="name=src/app/avatar/avatar.component.html"
/>
@Input()
set avatarUrl(url: string | undefined) {
if (url) {
this.downloadImage(url)
}
}
@Output() upload = new EventEmitter<string>()
constructor(
private readonly supabase: SupabaseService,
private readonly dom: DomSanitizer
) {
addIcons({ person })
}
async downloadImage(path: string) {
try {
const { data, error } = await this.supabase.downLoadImage(path)
if (error) {
throw error
}
this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data!))
} catch (error: any) {
console.error('Error downloading image: ', error.message)
}
}
async uploadAvatar() {
const loader = await this.supabase.createLoader()
try {
const photo = await Camera.getPhoto({
resultType: CameraResultType.DataUrl,
})
const file = await fetch(photo.dataUrl!)
.then((res) => res.blob())
.then((blob) => new File([blob], 'my-file', { type: `image/${photo.format}` }))
const fileName = `${Math.random()}-${new Date().getTime()}.${photo.format}`
await loader.present()
const { error } = await this.supabase.uploadAvatar(fileName, file)
if (error) {
throw error
}
this.upload.emit(fileName)
} catch (error: any) {
this.supabase.createNotice(error.message)
} finally {
loader.dismiss()
}
}
}
```
</$CodeTabs>
### Add the new widget
And then, we can add the widget on top of the `AccountComponent` HTML template:
<$CodeTabs>
```ts name=src/app/account.component.ts
template: `
<ion-header>
<ion-toolbar>
<ion-title>Account</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<app-avatar
[avatarUrl]="this.profile?.avatar_url"
(upload)="updateProfile($event)"
></app-avatar>
<!-- input fields -->
`
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-angular-user-management/src/app/avatar/avatar.component.scss"
lines={[[1, -1]]}
meta="name=src/app/avatar/avatar.component.scss"
/>
At this stage, you have a fully functional application!
@@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
@@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false
@@ -0,0 +1,47 @@
{
"root": true,
"ignorePatterns": ["projects/**/*"],
"overrides": [
{
"files": ["*.ts"],
"parserOptions": {
"project": ["tsconfig.json", "e2e/tsconfig.json"],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/ng-cli-compat",
"plugin:@angular-eslint/ng-cli-compat--formatting-add-on",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/component-class-suffix": [
"error",
{
"suffixes": ["Page", "Component"]
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
]
}
},
{
"files": ["*.html"],
"extends": ["plugin:@angular-eslint/template/recommended"],
"rules": {}
}
]
}
@@ -0,0 +1,32 @@
# Specifies intentionally untracked files to ignore when using Git
# http://git-scm.com/docs/gitignore
*~
*.sw[mnpcod]
.tmp
*.tmp
*.tmp.*
*.sublime-project
*.sublime-workspace
.DS_Store
Thumbs.db
UserInterfaceState.xcuserstate
$RECYCLE.BIN/
*.log
log.txt
npm-debug.log*
/.angular
/.idea
/.ionic
/.sass-cache
/.sourcemaps
/.versions
/.vscode
/coverage
/dist
/node_modules
/platforms
/plugins
/www
@@ -0,0 +1,133 @@
# Supabase Ionic Angular User Management
> This project is based on the Supabase Ionic Angular user management [Quickstart](https://supabase.io/docs/guides/with-ionic-angular)
> To keep it in sync with this Quickstart, architecture choices have been made to facilitate the comparison
>
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 profile information, including a profile image.
This demonstrates how to use:
- User signups using Supabase [Auth](https://supabase.io/auth).
- User avatar images using Supabase [Storage](https://supabase.io/storage).
- Frontend using [Angular](https://angular.io).
## Technologies used
- Frontend:
- [Ionic](https://ionicframework.com)
- [Capacitor](https://capacitorjs.com)
- [Angular](https://angular.io)
- [Supabase.js](https://supabase.io/docs/library/getting-started) for user management and realtime data syncing.
- Backend:
- [app.supabase.io](https://app.supabase.io/): hosted Postgres database with restful API for usage with Supabase.js.
## Instant deploy
The Vercel deployment will guide you through creating a Supabase account and project. After installation of the Supabase integration, all relevant environment variables will be set up so that the project is usable immediately after deployment 🚀.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?project-name=supabase-ionic-angular&repo-name=supabase-ionic-angular&envDescription=Find%20the%20Supabase%20URL%20and%20key%20in%20the%20your%20auto-generated%20docs%20at%20app.supabase.io&repository-url=https%3A%2F%2Fgithub.com%2Fmhartington%2Fsupabase-ionic-angular%2Ftree%2Fmain)
## Build from scratch
### 1. Create new project
Sign up to Supabase - [https://app.supabase.io](https://app.supabase.io) and create a new project. Wait for your database to start.
### 2. Run "User Management" Quickstart
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`.
### 3. Get the URL and Key
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.
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).
![image](https://user-images.githubusercontent.com/10214025/88916245-528c2680-d298-11ea-8a71-708f93e1ce4f.png)
**_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.
### 4. Env vars
Update your environment file `environment.ts`
```
export const environment = {
// ...
supabaseUrl: "YOUR_SUPBASE_URL",
supbaseKey: "YOUR_SUPABASE_KEY"
};
```
Populate this file with your URL and Key.
### 5. Run the application
Run the application: `ionic serve` and the browser will open to `https://localhost:8100/` and you are ready to go 🚀.
## Supabase details
### Postgres Row level security
This project uses very high-level Authorization using Postgres' Role Level Security.
When you start a Postgres database on Supabase, we populate it with an `auth` schema, and some helper functions.
When a user logs in, they are issued a JWT with the role `authenticated` and thier UUID.
We can use these details to provide fine-grained control over what each user can and cannot do.
This is a trimmed-down schema, with the policies:
```sql
-- Create a table for Public Profiles
create table profiles (
id uuid references auth.users not null,
updated_at timestamp with time zone,
username text unique,
avatar_url text,
website text,
primary key (id),
unique(username),
constraint username_length check (char_length(username) >= 3)
);
alter table profiles enable row level security;
create policy "Public profiles are viewable by everyone."
on profiles for select
using ( true );
create policy "Users can insert their own profile."
on profiles for insert
with check ( auth.uid() = id );
create policy "Users can update own profile."
on profiles for update
using ( auth.uid() = id );
-- Set up Realtime!
begin;
drop publication if exists supabase_realtime;
create publication supabase_realtime;
commit;
alter publication supabase_realtime add table profiles;
-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');
create policy "Avatar images are publicly accessible."
on storage.objects for select
using ( bucket_id = 'avatars' );
create policy "Anyone can upload an avatar."
on storage.objects for insert
with check ( bucket_id = 'avatars' );
```
## Authors
- [Supabase](https://supabase.io)
- [Mike Hartington](https://github.com/mhartington)
Supabase is open source. We'd love for you to follow along and get involved at https://github.com/supabase/supabase
@@ -0,0 +1,161 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"defaultProject": "app",
"newProjectRoot": "projects",
"projects": {
"app": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "www",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
}
],
"styles": ["src/theme/variables.scss", "src/global.scss"],
"scripts": [],
"aot": false,
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
},
"ci": {
"progress": false
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"buildTarget": "app:build"
},
"configurations": {
"production": {
"buildTarget": "app:build:production"
},
"ci": {
"progress": false
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "app:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"styles": [],
"scripts": [],
"assets": [
{
"glob": "favicon.ico",
"input": "src/",
"output": "/"
},
{
"glob": "**/*",
"input": "src/assets",
"output": "/assets"
}
]
},
"configurations": {
"ci": {
"progress": false,
"watch": false
}
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "app:serve"
},
"configurations": {
"production": {
"devServerTarget": "app:serve:production"
},
"ci": {
"devServerTarget": "app:serve:ci"
}
}
}
}
}
},
"cli": {
"defaultCollection": "@ionic/angular-toolkit",
"analytics": false
},
"schematics": {
"@ionic/angular-toolkit:component": {
"styleext": "scss"
},
"@ionic/angular-toolkit:page": {
"styleext": "scss"
}
}
}
@@ -0,0 +1,10 @@
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'io.ionic.starter',
appName: 'supabase-ionic-angular',
webDir: 'www',
bundledWebRuntime: false
};
export default config;
@@ -0,0 +1,37 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
SELENIUM_PROMISE_MANAGER: false,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.PRETTY
}
}));
}
};
@@ -0,0 +1,14 @@
import { AppPage } from './app.po';
describe('new App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should be blank', () => {
page.navigateTo();
expect(page.getParagraphText()).toContain('Start with Ionic UI Components');
});
});
@@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.deepCss('app-root ion-content')).getText();
}
}
@@ -0,0 +1,12 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"node"
]
}
}
@@ -0,0 +1,7 @@
{
"name": "supabase-ionic-angular",
"integrations": {
"capacitor": {}
},
"type": "angular"
}
@@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/ngv'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,69 @@
{
"name": "supabase-ionic-angular",
"version": "0.0.1",
"author": "Ionic Framework",
"homepage": "https://ionicframework.com/",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/common": "~21.2.0",
"@angular/core": "~21.2.0",
"@angular/forms": "~21.2.0",
"@angular/platform-browser": "~21.2.0",
"@angular/platform-browser-dynamic": "~21.2.0",
"@angular/router": "~21.2.0",
"@capacitor/app": "8.0.1",
"@capacitor/camera": "^8.0.1",
"@capacitor/core": "8.1.0",
"@capacitor/haptics": "8.0.1",
"@capacitor/keyboard": "8.0.1",
"@capacitor/status-bar": "8.0.1",
"@ionic/angular": "^8.7.18",
"@ionic/pwa-elements": "^3.4.0",
"@supabase/supabase-js": "^2.98.0",
"rxjs": "~7.8.2",
"tslib": "^2.8.1",
"zone.js": "~0.16.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "~21.2.0",
"@angular-eslint/builder": "~21.2.0",
"@angular-eslint/eslint-plugin": "~21.2.0",
"@angular-eslint/eslint-plugin-template": "~21.2.0",
"@angular-eslint/template-parser": "~21.2.0",
"@angular/cli": "~21.2.0",
"@angular/compiler": "~21.2.0",
"@angular/compiler-cli": "~21.2.0",
"@angular/language-service": "~21.2.0",
"@capacitor/cli": "8.1.0",
"@ionic/angular-toolkit": "^12.3.0",
"@types/jasmine": "~6.0.0",
"@types/jasminewd2": "~2.0.13",
"@types/node": "^25.3.3",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"eslint": "^9.0.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jsdoc": "62.7.1",
"eslint-plugin-prefer-arrow": "1.2.3",
"jasmine-core": "~6.1.0",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.2.0",
"protractor": "~7.0.0",
"ts-node": "~10.9.2",
"typescript": "~5.9.3"
},
"description": "An Ionic project"
}
@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AccountPage } from './account.page';
const routes: Routes = [
{
path: '',
component: AccountPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AccountPageRoutingModule {}
@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { AccountPageRoutingModule } from './account-routing.module';
import { AccountPage } from './account.page';
import { AvatarComponentModule } from '../avatar/avatar.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
AccountPageRoutingModule,
AvatarComponentModule
],
declarations: [AccountPage]
})
export class AccountPageModule {}
@@ -0,0 +1,46 @@
<ion-header>
<ion-toolbar>
<ion-title>Account</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<app-avatar
[avatarUrl]="this.profile?.avatar_url"
(upload)="updateProfile($event)"
>
</app-avatar>
<form>
<ion-item>
<ion-label position="stacked">Email</ion-label>
<ion-input type="email" name="email" [(ngModel)]="email" readonly></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Name</ion-label>
<ion-input
type="text"
name="username"
[(ngModel)]="profile.username"
></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Website</ion-label>
<ion-input
type="url"
name="website"
[(ngModel)]="profile.website"
></ion-input>
</ion-item>
<div class="ion-text-center">
<ion-button fill="clear" (click)="updateProfile()"
>Update Profile</ion-button
>
</div>
</form>
<div class="ion-text-center">
<ion-button fill="clear" (click)="signOut()">Log Out</ion-button>
</div>
</ion-content>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { AccountPage } from './account.page';
describe('AccountPage', () => {
let component: AccountPage;
let fixture: ComponentFixture<AccountPage>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ AccountPage ],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(AccountPage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,69 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Profile, SupabaseService } from '../supabase.service';
@Component({
selector: 'app-account',
standalone: false,
templateUrl: './account.page.html',
styleUrls: ['./account.page.scss'],
})
export class AccountPage implements OnInit {
profile: Profile = {
username: '',
avatar_url: '',
website: '',
};
email = '';
constructor(
private readonly supabase: SupabaseService,
private router: Router
) {}
ngOnInit() {
this.getEmail();
this.getProfile();
}
async getEmail() {
this.email = await this.supabase.user.then((user) => user?.email || '');
}
async getProfile() {
try {
const { data: profile, error, status } = await this.supabase.profile;
if (error && status !== 406) {
throw error;
}
if (profile) {
this.profile = profile;
}
} catch (error: any) {
alert(error.message);
}
}
async updateProfile(avatar_url: string = '') {
const loader = await this.supabase.createLoader();
await loader.present();
try {
const { error } = await this.supabase.updateProfile({ ...this.profile, avatar_url });
if (error) {
throw error;
}
await loader.dismiss();
await this.supabase.createNotice('Profile updated!');
} catch (error: any) {
await loader.dismiss();
await this.supabase.createNotice(error.message);
}
}
async signOut() {
console.log('testing?');
await this.supabase.signOut();
this.router.navigate(['/'], { replaceUrl: true });
}
}
@@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
loadChildren: () =>
import('./login/login.module').then((m) => m.LoginPageModule),
},
{
path: 'account',
loadChildren: () =>
import('./account/account.module').then((m) => m.AccountPageModule),
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
@@ -0,0 +1,3 @@
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>
@@ -0,0 +1,23 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
// TODO: add more tests!
});
@@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { SupabaseService } from './supabase.service';
@Component({
selector: 'app-root',
standalone: false,
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor(private supabase: SupabaseService, private router: Router) {
this.supabase.authChanges((_, session) => {
console.log(session);
if (session?.user) {
this.router.navigate(['/account']);
}
});
}
}
@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}
@@ -0,0 +1,6 @@
<div class="avatar_wrapper" (click)="uploadAvatar()">
<img *ngIf="_avatarUrl; else noAvatar" [src]="_avatarUrl" />
<ng-template #noAvatar>
<ion-icon name="person" class="no-avatar"></ion-icon>
</ng-template>
</div>
@@ -0,0 +1,30 @@
:host {
display: block;
margin: auto;
min-height: 150px;
.avatar_wrapper {
margin: 16px auto 16px;
border-radius: 50%;
overflow: hidden;
height: 150px;
aspect-ratio: 1/1;
background: var(--ion-color-step-50);
border: thick solid var(--ion-color-step-200);
&:hover {
cursor: pointer;
}
ion-icon.no-avatar {
width: 100%;
height: 115%;
}
}
img {
display: block;
object-fit: cover;
width: 100%;
height: 100%;
}
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { AvatarComponent } from './avatar.component';
describe('AvatarComponent', () => {
let component: AvatarComponent;
let fixture: ComponentFixture<AvatarComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ AvatarComponent ],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(AvatarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,75 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { SupabaseService } from '../supabase.service';
import { Camera, CameraResultType } from '@capacitor/camera';
import { addIcons } from 'ionicons';
import { person } from 'ionicons/icons';
@Component({
selector: 'app-avatar',
standalone: false,
templateUrl: './avatar.component.html',
styleUrls: ['./avatar.component.scss'],
})
export class AvatarComponent {
_avatarUrl: SafeResourceUrl | undefined;
uploading = false;
@Input()
set avatarUrl(url: string | undefined) {
if (url) {
this.downloadImage(url);
}
}
@Output() upload = new EventEmitter<string>();
constructor(
private readonly supabase: SupabaseService,
private readonly dom: DomSanitizer
) {
addIcons({ person });
}
async downloadImage(path: string) {
try {
const { data, error } = await this.supabase.downLoadImage(path);
if (error) {
throw error;
}
this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(
URL.createObjectURL(data!)
);
} catch (error: any) {
console.error('Error downloading image: ', error.message);
}
}
async uploadAvatar() {
const loader = await this.supabase.createLoader();
try {
const photo = await Camera.getPhoto({
resultType: CameraResultType.DataUrl,
});
const file = await fetch(photo.dataUrl!)
.then((res) => res.blob())
.then((blob) => new File([blob], 'my-file', { type: `image/${photo.format}` }));
const fileName = `${Math.random()}-${new Date().getTime()}.${photo.format}`;
await loader.present();
const { error } = await this.supabase.uploadAvatar(fileName, file);
if (error) {
throw error;
}
this.upload.emit(fileName);
} catch (error: any) {
this.supabase.createNotice(error.message);
} finally {
loader.dismiss();
}
}
}
@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { AvatarComponent } from './avatar.component';
@NgModule({
imports: [ CommonModule, FormsModule, IonicModule,],
declarations: [AvatarComponent],
exports: [AvatarComponent]
})
export class AvatarComponentModule {}
@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomePage } from './home.page';
const routes: Routes = [
{
path: '',
component: HomePage,
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomePageRoutingModule {}
@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { HomePage } from './home.page';
import { HomePageRoutingModule } from './home-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
HomePageRoutingModule
],
declarations: [HomePage]
})
export class HomePageModule {}
@@ -0,0 +1,20 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Blank
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Blank</ion-title>
</ion-toolbar>
</ion-header>
<div id="container">
<strong>Ready to create an app?</strong>
<p>Start with Ionic <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
</div>
</ion-content>
@@ -0,0 +1,27 @@
#container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
#container strong {
font-size: 20px;
line-height: 26px;
}
#container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
#container a {
text-decoration: none;
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { HomePage } from './home.page';
describe('HomePage', () => {
let component: HomePage;
let fixture: ComponentFixture<HomePage>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ HomePage ],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(HomePage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,13 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
standalone: false,
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
constructor() {}
}
@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginPage } from './login.page';
const routes: Routes = [
{
path: '',
component: LoginPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class LoginPageRoutingModule {}
@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { LoginPageRoutingModule } from './login-routing.module';
import { LoginPage } from './login.page';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
LoginPageRoutingModule
],
declarations: [LoginPage]
})
export class LoginPageModule {}
@@ -0,0 +1,28 @@
<ion-header>
<ion-toolbar>
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<h1>Supabase + Ionic Angular</h1>
<p>Sign in via magic link with your email below</p>
</div>
<ion-list inset="true">
<form (ngSubmit)="handleLogin($event)">
<ion-item>
<ion-label position="stacked">Email</ion-label>
<ion-input
[(ngModel)]="email"
name="email"
autocomplete
type="email"
></ion-input>
</ion-item>
<div class="ion-text-center">
<ion-button type="submit" fill="clear">Login</ion-button>
</div>
</form>
</ion-list>
</ion-content>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { LoginPage } from './login.page';
describe('LoginPage', () => {
let component: LoginPage;
let fixture: ComponentFixture<LoginPage>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ LoginPage ],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(LoginPage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,31 @@
import { Component, OnInit } from '@angular/core';
import { SupabaseService } from '../supabase.service';
@Component({
selector: 'app-login',
standalone: false,
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss'],
})
export class LoginPage {
email = '';
constructor(private readonly supabase: SupabaseService) {}
async handleLogin(event: any) {
event.preventDefault();
const loader = await this.supabase.createLoader();
await loader.present();
try {
const { error } = await this.supabase.signIn(this.email);
if (error) {
throw error;
}
await loader.dismiss();
await this.supabase.createNotice('Check your email for the login link!');
} catch (error: any) {
await loader.dismiss();
await this.supabase.createNotice(error.error_description || error.message);
}
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SupabaseService } from './supabase.service';
describe('SupabaseService', () => {
let service: SupabaseService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SupabaseService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,96 @@
import { Injectable } from '@angular/core';
import { LoadingController, ToastController } from '@ionic/angular';
import {
AuthChangeEvent,
createClient,
Session,
SupabaseClient,
} from '@supabase/supabase-js';
import { environment } from '../environments/environment';
export interface Profile {
username: string;
website: string;
avatar_url: string;
}
@Injectable({
providedIn: 'root',
})
export class SupabaseService {
private supabase: SupabaseClient;
constructor(private loadingCtrl: LoadingController, private toastCtrl: ToastController) {
this.supabase = createClient(
environment.supabaseUrl,
environment.supabaseKey
);
}
get user() {
return this.supabase.auth.getUser().then(({ data }) => data?.user);
}
get session() {
return this.supabase.auth
.getClaims()
.then(async ({ data }) => {
if (!data?.claims) {
return null;
}
const { data: userData } = await this.supabase.auth.getUser();
return userData?.user ? ({ user: userData.user } as Session) : null;
});
}
get profile() {
return this.user
.then((user) => user?.id)
.then((id) =>
this.supabase.from('profiles').select(`username, website, avatar_url`).eq('id', id).single()
);
}
authChanges(
callback: (event: AuthChangeEvent, session: Session | null) => void
) {
return this.supabase.auth.onAuthStateChange(callback);
}
signIn(email: string) {
return this.supabase.auth.signInWithOtp({ email });
}
signOut() {
return this.supabase.auth.signOut();
}
async updateProfile(profile: Profile) {
const user = await this.user;
const update = {
...profile,
id: user?.id,
updated_at: new Date(),
};
return this.supabase.from('profiles').upsert(update);
}
downLoadImage(path: string) {
return this.supabase.storage.from('avatars').download(path);
}
uploadAvatar(filePath: string, file: File) {
return this.supabase.storage.from('avatars').upload(filePath, file);
}
async createNotice(message: string) {
const toast = await this.toastCtrl.create({ message, duration: 5000 });
await toast.present();
}
createLoader() {
return this.loadingCtrl.create();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

@@ -0,0 +1 @@
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,5 @@
export const environment = {
production: true,
supabaseUrl: "",
supabaseKey: ""
};
@@ -0,0 +1,18 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
supabaseUrl: "",
supabaseKey: ""
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
@@ -0,0 +1,26 @@
/*
* App Global CSS
* ----------------------------------------------------------------------------
* Put style rules here that you want to apply globally. These styles are for
* the entire app and not just one component. Additionally, this file can be
* used as an entry point to import other CSS/Sass files to be included in the
* output CSS.
* For more information on global stylesheets, visit the documentation:
* https://ionicframework.com/docs/layout/global-stylesheets
*/
/* Core CSS required for Ionic components to work properly */
@import "~@ionic/angular/css/core.css";
/* Basic CSS for apps built with Ionic */
@import "~@ionic/angular/css/normalize.css";
@import "~@ionic/angular/css/structure.css";
@import "~@ionic/angular/css/typography.css";
@import '~@ionic/angular/css/display.css';
/* Optional CSS utils that can be commented out */
@import "~@ionic/angular/css/padding.css";
@import "~@ionic/angular/css/float-elements.css";
@import "~@ionic/angular/css/text-alignment.css";
@import "~@ionic/angular/css/text-transformation.css";
@import "~@ionic/angular/css/flex-utils.css";
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Ionic App</title>
<base href="/" />
<meta name="color-scheme" content="light dark" />
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<link rel="icon" type="image/png" href="assets/icon/favicon.png" />
<!-- add to homescreen for ios -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
</head>
<body>
<app-root></app-root>
</body>
</html>
@@ -0,0 +1,14 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { defineCustomElements } from '@ionic/pwa-elements/loader';
defineCustomElements(window)
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
@@ -0,0 +1,65 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
import './zone-flags';
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
@@ -0,0 +1,25 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
<T>(id: string): T;
keys(): string[];
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
@@ -0,0 +1,236 @@
// Ionic Variables and Theming. For more info, please see:
// http://ionicframework.com/docs/theming/
/** Ionic CSS Variables **/
:root {
/** primary **/
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
/** secondary **/
--ion-color-secondary: #3dc2ff;
--ion-color-secondary-rgb: 61, 194, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #36abe0;
--ion-color-secondary-tint: #50c8ff;
/** tertiary **/
--ion-color-tertiary: #5260ff;
--ion-color-tertiary-rgb: 82, 96, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #4854e0;
--ion-color-tertiary-tint: #6370ff;
/** success **/
--ion-color-success: #2dd36f;
--ion-color-success-rgb: 45, 211, 111;
--ion-color-success-contrast: #ffffff;
--ion-color-success-contrast-rgb: 255, 255, 255;
--ion-color-success-shade: #28ba62;
--ion-color-success-tint: #42d77d;
/** warning **/
--ion-color-warning: #ffc409;
--ion-color-warning-rgb: 255, 196, 9;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0, 0, 0;
--ion-color-warning-shade: #e0ac08;
--ion-color-warning-tint: #ffca22;
/** danger **/
--ion-color-danger: #eb445a;
--ion-color-danger-rgb: 235, 68, 90;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #cf3c4f;
--ion-color-danger-tint: #ed576b;
/** dark **/
--ion-color-dark: #222428;
--ion-color-dark-rgb: 34, 36, 40;
--ion-color-dark-contrast: #ffffff;
--ion-color-dark-contrast-rgb: 255, 255, 255;
--ion-color-dark-shade: #1e2023;
--ion-color-dark-tint: #383a3e;
/** medium **/
--ion-color-medium: #92949c;
--ion-color-medium-rgb: 146, 148, 156;
--ion-color-medium-contrast: #ffffff;
--ion-color-medium-contrast-rgb: 255, 255, 255;
--ion-color-medium-shade: #808289;
--ion-color-medium-tint: #9d9fa6;
/** light **/
--ion-color-light: #f4f5f8;
--ion-color-light-rgb: 244, 245, 248;
--ion-color-light-contrast: #000000;
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #d7d8da;
--ion-color-light-tint: #f5f6f9;
}
@media (prefers-color-scheme: dark) {
/*
* Dark Colors
* -------------------------------------------
*/
body {
--ion-color-primary: #428cff;
--ion-color-primary-rgb: 66,140,255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255,255,255;
--ion-color-primary-shade: #3a7be0;
--ion-color-primary-tint: #5598ff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80,200,255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255,255,255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106,100,255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255,255,255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47,223,117;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0,0,0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255,213,52;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0,0,0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255,73,97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255,255,255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244,245,248;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0,0,0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152,154,162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0,0,0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #222428;
--ion-color-light-rgb: 34,36,40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255,255,255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
}
/*
* iOS Dark Theme
* -------------------------------------------
*/
.ios body {
--ion-background-color: #000000;
--ion-background-color-rgb: 0,0,0;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-color-step-50: #0d0d0d;
--ion-color-step-100: #1a1a1a;
--ion-color-step-150: #262626;
--ion-color-step-200: #333333;
--ion-color-step-250: #404040;
--ion-color-step-300: #4d4d4d;
--ion-color-step-350: #595959;
--ion-color-step-400: #666666;
--ion-color-step-450: #737373;
--ion-color-step-500: #808080;
--ion-color-step-550: #8c8c8c;
--ion-color-step-600: #999999;
--ion-color-step-650: #a6a6a6;
--ion-color-step-700: #b3b3b3;
--ion-color-step-750: #bfbfbf;
--ion-color-step-800: #cccccc;
--ion-color-step-850: #d9d9d9;
--ion-color-step-900: #e6e6e6;
--ion-color-step-950: #f2f2f2;
--ion-item-background: #000000;
--ion-card-background: #1c1c1d;
}
.ios ion-modal {
--ion-background-color: var(--ion-color-step-100);
--ion-toolbar-background: var(--ion-color-step-150);
--ion-toolbar-border-color: var(--ion-color-step-250);
}
/*
* Material Design Dark Theme
* -------------------------------------------
*/
.md body {
--ion-background-color: #121212;
--ion-background-color-rgb: 18,18,18;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-border-color: #222222;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
--ion-color-step-150: #363636;
--ion-color-step-200: #414141;
--ion-color-step-250: #4d4d4d;
--ion-color-step-300: #595959;
--ion-color-step-350: #656565;
--ion-color-step-400: #717171;
--ion-color-step-450: #7d7d7d;
--ion-color-step-500: #898989;
--ion-color-step-550: #949494;
--ion-color-step-600: #a0a0a0;
--ion-color-step-650: #acacac;
--ion-color-step-700: #b8b8b8;
--ion-color-step-750: #c4c4c4;
--ion-color-step-800: #d0d0d0;
--ion-color-step-850: #dbdbdb;
--ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3;
--ion-item-background: #1e1e1e;
--ion-toolbar-background: #1f1f1f;
--ion-tab-bar-background: #1f1f1f;
--ion-card-background: #1e1e1e;
}
}
@@ -0,0 +1,6 @@
/**
* Prevents Angular change detection from
* running with certain Web Component callbacks
*/
// eslint-disable-next-line no-underscore-dangle
(window as any).__Zone_disable_customElements = true;
@@ -0,0 +1,15 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": ["node"]
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}
@@ -0,0 +1,26 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"useDefineForClassFields": false,
"module": "es2020",
"types": ["node"],
"skipLibCheck": true,
"lib": ["ES2022", "dom"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
@@ -0,0 +1,18 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}