Files
supabase/apps/docs/content/guides/getting-started/tutorials/with-ionic-vue.mdx
Tyler 6b3420a5fd docs: Remove storage management section from the quick starts. (#21985)
remove storage management to keep the guide simple
2024-03-16 08:03:06 +09:00

557 lines
14 KiB
Plaintext

---
title: 'Build a User Management App with Ionic Vue'
description: 'Learn how to use Supabase in your Ionic Vue App.'
---
<QuickstartIntro />
![Supabase User Management example](/docs/img/ionic-demos/ionic-angular-account.png)
<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-vue).
</Admonition>
<ProjectSetup />
## Building the app
Let's start building the Vue app from scratch.
### Initialize an Ionic Vue app
We can use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize
an app called `supabase-ionic-vue`:
```bash
npm install -g @ionic/cli
ionic start supabase-ionic-vue blank --type vue
cd supabase-ionic-vue
```
Then let's 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 a `.env`.
All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
```bash .env
VUE_APP_SUPABASE_URL=YOUR_SUPABASE_URL
VUE_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
```
Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client. 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.
```js src/supabase.ts"
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.VUE_APP_SUPABASE_URL as string;
const supabaseAnonKey = process.env.VUE_APP_SUPABASE_ANON_KEY as string;
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
```
### Set up a login route
Let's set up a Vue component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
```html /src/views/Login.vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<h1>Supabase + Ionic Vue</h1>
<p>Sign in via magic link with your email below</p>
</div>
<ion-list inset="true">
<form @submit.prevent="handleLogin">
<ion-item>
<ion-label position="stacked">Email</ion-label>
<ion-input v-model="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>
<p>{{email}}</p>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { supabase } from '../supabase'
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonList,
IonItem,
IonLabel,
IonInput,
IonButton,
toastController,
loadingController,
} from '@ionic/vue'
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'LoginPage',
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonList,
IonItem,
IonLabel,
IonInput,
IonButton,
},
setup() {
const email = ref('')
const handleLogin = async () => {
const loader = await loadingController.create({})
const toast = await toastController.create({ duration: 5000 })
try {
await loader.present()
const { error } = await supabase.auth.signIn({ email: email.value })
if (error) throw error
toast.message = 'Check your email for the login link!'
await toast.present()
} catch (error: any) {
toast.message = error.error_description || error.message
await toast.present()
} finally {
await loader.dismiss()
}
}
return { handleLogin, email }
},
})
</script>
```
### Account page
After a user is signed in we can allow them to edit their profile details and manage their account.
Let's create a new component for that called `Account.vue`.
```html src/views/Account.vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Account</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form @submit.prevent="updateProfile">
<ion-item>
<ion-label>
<p>Email</p>
<p>{{ session?.user?.email }}</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label position="stacked">Name</ion-label>
<ion-input type="text" name="username" v-model="profile.username"></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Website</ion-label>
<ion-input type="url" name="website" v-model="profile.website"></ion-input>
</ion-item>
<div class="ion-text-center">
<ion-button fill="clear" type="submit">Update Profile</ion-button>
</div>
</form>
<div class="ion-text-center">
<ion-button fill="clear" @click="signOut">Log Out</ion-button>
</div>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { store } from '@/store'
import { supabase } from '@/supabase'
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
toastController,
loadingController,
IonInput,
IonItem,
IonButton,
IonLabel,
} from '@ionic/vue'
import { User } from '@supabase/supabase-js'
import { defineComponent, onMounted, ref } from 'vue'
export default defineComponent({
name: 'AccountPage',
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonInput,
IonItem,
IonButton,
IonLabel,
},
setup() {
const session = ref(supabase.auth.session())
const profile = ref({
username: '',
website: '',
avatar_url: '',
})
const user = store.user as User
async function getProfile() {
const loader = await loadingController.create({})
const toast = await toastController.create({ duration: 5000 })
await loader.present()
try {
const { data, error, status } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.id)
.single()
if (error && status !== 406) throw error
if (data) {
console.log(data)
profile.value = {
username: data.username,
website: data.website,
avatar_url: data.avatar_url,
}
}
} catch (error: any) {
toast.message = error.message
await toast.present()
} finally {
await loader.dismiss()
}
}
const updateProfile = async () => {
const loader = await loadingController.create({})
const toast = await toastController.create({ duration: 5000 })
try {
await loader.present()
const updates = {
id: user.id,
...profile.value,
updated_at: new Date(),
}
//
const { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
})
//
if (error) throw error
} catch (error: any) {
toast.message = error.message
await toast.present()
} finally {
await loader.dismiss()
}
}
async function signOut() {
const loader = await loadingController.create({})
const toast = await toastController.create({ duration: 5000 })
await loader.present()
try {
const { error } = await supabase.auth.signOut()
if (error) throw error
} catch (error: any) {
toast.message = error.message
await toast.present()
} finally {
await loader.dismiss()
}
}
onMounted(() => {
getProfile()
})
return { signOut, profile, session, updateProfile }
},
})
</script>
```
### Launch!
Now that we have all the components in place, let's update `App.vue` and our routes:
```ts src/router.index.ts
import { createRouter, createWebHistory } from '@ionic/vue-router'
import { RouteRecordRaw } from 'vue-router'
import LoginPage from '../views/Login.vue'
import AccountPage from '../views/Account.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Login',
component: LoginPage,
},
{
path: '/account',
name: 'Account',
component: AccountPage,
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
})
export default router
```
```html src/App.vue
<template>
<ion-app>
<ion-router-outlet />
</ion-app>
</template>
<script lang="ts">
import { IonApp, IonRouterOutlet, useIonRouter } from '@ionic/vue'
import { defineComponent } from 'vue'
import { store } from './store'
import { supabase } from './supabase'
export default defineComponent({
name: 'App',
components: {
IonApp,
IonRouterOutlet,
},
setup() {
const router = useIonRouter()
store.user = supabase.auth.user() ?? {}
supabase.auth.onAuthStateChange((_, session) => {
store.user = session?.user ?? {}
if (session?.user) {
router.replace('/account')
}
})
},
})
</script>
```
Once that's done, run this in a terminal window:
```bash
ionic serve
```
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
![Supabase Ionic Vue](/docs/img/ionic-demos/ionic-vue.png)
## Bonus: Profile photos
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos.
### Create an upload widget
First install two packages in order to interact with the user's camera.
```bash
npm install @ionic/pwa-elements @capacitor/camera
```
[CapacitorJS](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.
With those packages installed we can update our `main.ts` to include an additional bootstrapping call for the Ionic PWA Elements.
```ts src/main.tsx"
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { IonicVue } from '@ionic/vue'
/* Core CSS required for Ionic components to work properly */
import '@ionic/vue/css/ionic.bundle.css'
/* Theme variables */
import './theme/variables.css'
import { defineCustomElements } from '@ionic/pwa-elements/loader'
defineCustomElements(window)
const app = createApp(App).use(IonicVue).use(router)
router.isReady().then(() => {
app.mount('#app')
})
```
Then create an **AvatarComponent**.
```html src/components/Avatar.vue
<template>
<div class="avatar">
<div class="avatar_wrapper" @click="uploadAvatar">
<img v-if="avatarUrl" :src="avatarUrl" />
<ion-icon v-else name="person" class="no-avatar"></ion-icon>
</div>
</div>
</template>
<script lang="ts">
import { ref, toRefs, watch, defineComponent } from 'vue'
import { supabase } from '../supabase'
import { Camera, CameraResultType } from '@capacitor/camera'
import { IonIcon } from '@ionic/vue'
import { person } from 'ionicons/icons'
export default defineComponent({
name: 'AppAvatar',
props: { path: String },
emits: ['upload', 'update:path'],
components: { IonIcon },
setup(prop, { emit }) {
const { path } = toRefs(prop)
const avatarUrl = ref('')
const downloadImage = async () => {
try {
const { data, error } = await supabase.storage.from('avatars').download(path.value)
if (error) throw error
avatarUrl.value = URL.createObjectURL(data!)
} catch (error: any) {
console.error('Error downloading image: ', error.message)
}
}
const uploadAvatar = async () => {
try {
const photo = await Camera.getPhoto({
resultType: CameraResultType.DataUrl,
})
if (photo.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}`
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(fileName, file)
if (uploadError) {
throw uploadError
}
emit('update:path', fileName)
emit('upload')
}
} catch (error) {
console.log(error)
}
}
watch(path, () => {
if (path.value) downloadImage()
})
return { avatarUrl, uploadAvatar, person }
},
})
</script>
<style>
.avatar {
display: block;
margin: auto;
min-height: 150px;
}
.avatar .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);
}
.avatar .avatar_wrapper:hover {
cursor: pointer;
}
.avatar .avatar_wrapper ion-icon.no-avatar {
width: 100%;
height: 115%;
}
.avatar img {
display: block;
object-fit: cover;
width: 100%;
height: 100%;
}
</style>
```
### Add the new widget
And then we can add the widget to the Account page:
```html src/views/Account.vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Account</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<avatar v-model:path="profile.avatar_url" @upload="updateProfile"></avatar>
...
</template>
<script lang="ts">
import Avatar from '../components/Avatar.vue';
export default defineComponent({
name: 'AccountPage',
components: {
Avatar,
....
}
</script>
```
At this stage you have a fully functional application!