Nuxt3 and Pinia: how to save async API data to the store - vuejs3

I created a Pinia store file to retrieve invoices information from the Node.js API I created available on a public API address
import { mande } from "mande";
import { acceptHMRUpdate, defineStore } from "pinia";
import { useUsersStore } from "./user";
const api = mande("http://xxx.xxx.xxx.xxx/"); // hiding the IP address
const usersStore = useUsersStore();
await usersStore.signIn("test#gmail.com", "password");
api.options.headers.Authorization = "Bearer " + usersStore.getAccessToken;
export const useInvoicesStore = defineStore("invoices", {
state: () => ({
invoices: <any>[] || [],
invoice: null,
loading: false,
}),
getters: {
getInvoices: (state) => state.invoices,
getInvoice: (state) => state.invoice,
},
actions: {
async fetchInvoices() {
this.invoices = [];
this.loading = true;
try {
this.invoices = (await api.get("invoices")) as any[];
} catch (error) {
console.log(error);
} finally {
this.loading = false;
}
},
async fetchInvoice(id: string) {
this.invoice = null;
this.loading = true;
try {
this.invoice = (await api.get(`invoices/${id}`)) as any;
} catch (error) {
console.log(error);
} finally {
this.loading = false;
}
},
async createInvoice(invoice: any) {
this.loading = true;
try {
await api.post("invoices", invoice);
} catch (error) {
console.log(error);
} finally {
this.loading = false;
}
},
async updateInvoice(id: string, invoice: any) {
this.loading = true;
try {
await api.patch(`invoices/${id}`, invoice);
} catch (error) {
console.log(error);
} finally {
this.loading = false;
}
},
},
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUsersStore, import.meta.hot));
}
I use the store in a Nuxt3 page
<script setup>
const store = useInvoicesStore();
definePageMeta({
layout: "app",
});
let invoices = [];
await store.fetchInvoices();
invoices = store.getInvoices;
</script>
<template>
<div>
<main>
{{ invoices }}
<div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
<AppInvoiceList :invoices="invoices" />
</div>
</main>
</div>
</template>
I print the entire JSON (invoices) on the UI to understand whether the information is fetched from the server. What happens is that, once I hit reload (F5), for a split second the data appears on the screen. After that, the array is empty and the store as well.
How can I correctly save the data coming from the API in the Pinia store?

This is not the purpose of a Pinia store, it does not give out of the box persisted store states, it is only for providing central state management during the uninterrupted life span of the PWA.
There are two ways I can think of to persist the central state between reloads.
Option 1
Use $subscribe to save Pinia states to the browsers localStorage or indexDB using them as a cache, then on first load check localStorage for anything to restore back to the Pinia state else query the backend, you will need to consider a cache timeout mechanism.
https://pinia.vuejs.org/core-concepts/state.html#subscribing-to-the-state
There may be a persisted state pinia plugin available to do this for you, try searching for a solution.
Option 2
Service worker API - You may not need panini at all.
https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
https://developer.mozilla.org/en-US/docs/Web/API/Cache

Related

How to get stripe customers in next js

I am using Stripe in my NextJs project and have tried to get customers list in my app but have not succeeded. If anyone knows how to get it, please instruct me on how to do that.
This is my code:
import { loadStripe } from "#stripe/stripe-js";
async function getStripeCustomers(){
const stripe = await loadStripe(
process.env.key
);
if (stripe) {
// there was a toturail for node.js like this.
console.log(stripe.customers.list())
}
}
useEffect(() => {
getStripeCustomers()
}, []);
I think you should do this logic in backend so create a route in api folder then try this code.
// api/payment/get-all-customers.js
import Stripe from "stripe";
export default async function handler(req, res) {
if (req.method === "POST") {
const { token } = JSON.parse(req.body);
if (!token) {
return res.status(403).json({ msg: "Forbidden" });
}
const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET, {
apiVersion: "2020-08-27",
});
try {
const customers = await stripe.customers.list(); // returns all customers sorted by createdDate
res.status(200).json(customers);
} catch (err) {
console.log(err);
res.status(500).json({ error: true });
}
}
}
Now from frontend send a POST request to newly created route.

Nuxt async not work on page reload - firebase

I have a issue with asyncData() when i refresh the page. If I navigate from list to single item, it work, but if i reload the page i will see an empty object.
In my page i have this :
<template>
<div>
{{ getItem}}
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
data: () => ({
}),
computed: {
...mapState([
'single_item'
]),
getItem() {
return this.single_item
}
},
async asyncData({app,route, params,store}) {
let type = 'posts'
let id = params.id
return store.dispatch('fetchFirebaseSingle', {type,id })
}
}
</script>
in store.js
import { db } from '~/plugins/firebase'
const actions = {
....
async fetchFirebaseSingle({commit}, {type, id}) {
try {
console.log('fetchFirebaseSingle', type)
const docRef = await db.collection(type).doc(id)
docRef.get()
.then((doc) => {
if (doc.exists) {
const file = doc.data()
commit('SET_PAGE_SINGLE', file)
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
})
.catch((error) => {
console.log("Error getting document:", error);
});
} catch (e) {
console.log("Error getting document:", e);
}
},
}
const mutations = {
...
// Set Single Item
SET_PAGE_SINGLE ( state, single_item) {
state.single_item = single_item
},
},
const state = () => ({
single_item : {},
})
I tryed also to call directly from this page the database, but i have same issue. Did someone get similar issue with vuex and firebase or asyncData ?
Thanks
Nothing special here, asyncData is not supposed to work on page reload or a refesh (F5) but only with page transitions.
Unlike fetch, the promise returned by the asyncData hook is resolved during route transition
You could use the fetch() hook if you don't mind a non-blocking loading.
More info here: https://nuxtjs.org/docs/features/data-fetching#data-fetching

Get supabase `user` server side in next.js

I am attempting to get the current logged in supabase user while server side.
I have attempted to use const user = supabase.auth.user(); but I always get a null response.
I have also attempted const user = supabase.auth.getUserByCookie(req) but it also returns null. I think because I am not sending a cookie to the api when calling it from the hook.
I have tried passing the user.id from the hook to the api but the api is not receiving the parameters.
I also attempted this approach but the token is never fetched. It seems to not exist in req.cookies.
let supabase = createClient(supabaseUrl, supabaseKey);
let token = req.cookies['sb:token'];
if (!token) {
return
}
let authRequestResult = await fetch(`${supabaseUrl}/auth/v1/user`, {
headers: {
'Authorization': `Bearer ${token}`,
'APIKey': supabaseKey
}
});
`
Does anyone know how to get the current logged in user in server side code?
If you need to get the user in server-side, you need to set the Auth Cookie in the server using the given Next.js API.
// pages/api/auth.js
import { supabase } from "../path/to/supabaseClient/definition";
export default function handler(req, res) {
if (req.method === "POST") {
supabase.auth.api.setAuthCookie(req, res);
} else {
res.setHeader("Allow", ["POST"]);
res.status(405).json({
message: `Method ${req.method} not allowed`,
});
}
}
This endpoint needs to be called every time the state of the user is changed, i.e. the events SIGNED_IN and SIGNED_OUT
You can set up a useEffect in _app.js or probably in a User Context file.
// _app.js
import "../styles/globals.css";
import { supabase } from '../path/to/supabaseClient/def'
function MyApp({ Component, pageProps }) {
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
handleAuthChange(event, session)
if (event === 'SIGNED_IN') {
// TODO: Actions to Perform on Sign In
}
if (event === 'SIGNED_OUT') {
// TODO: Actions to Perform on Logout
}
})
checkUser()
return () => {
authListener.unsubscribe()
}
}, [])
return <Component {...pageProps} />;
}
async function handleAuthChange(event, session) {
await fetch('/api/auth', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'same-origin',
body: JSON.stringify({ event, session }),
})
}
export default MyApp;
You can now handle this user with a state and pass it to the app or whichever way you'd like to.
You can get the user in the server-side in any Next.js Page
// pages/user_route.js
import { supabase } from '../path/to/supabaseClient/def'
export default function UserPage ({ user }) {
return (
<h1>Email: {user.email}</h1>
)
}
export async function getServerSideProps({ req }) {
const { user } = await supabase.auth.api.getUserByCookie(req)
if (!user) {
return { props: {}, redirect: { destination: '/sign-in' } }
}
return { props: { user } }
}
Here's a YouTube Tutorial from Nader Dabit - https://www.youtube.com/watch?v=oXWImFqsQF4
And his GitHub Repository - https://github.com/dabit3/supabase-nextjs-auth
supabase have a library of helpers for managing auth for both client- and server-side auth and fetching in a couple of frameworks including Next.js: https://github.com/supabase/auth-helpers and appears to be the recommended solution for similar problems based on this thread: https://github.com/supabase/supabase/issues/3783
This is how I'm using it in an API handler, but provided you have access to req, you can access the user object this way:
import { supabaseServerClient } from '#supabase/auth-helpers-nextjs';
const { user } = await supabaseServerClient({ req, res }).auth.api.getUser(req.cookies["sb-access-token"]);
Note that you will need to use the helper library supabaseClient and supabaseServerClient on the client and server side respectively for this to work as intended.
I was following a tutorial today and was having a similar issue and the below is how i managed to fix it.
I've got this package installed github.com/jshttp/cookie which is why i'm calling cookie.parse.
Supabase Instance:
`//../../../utils/supabase`
import { createClient } from "#supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_KEY
);
In my case this was my API page:
import { supabase } from "../../../utils/supabase";
import cookie from "cookie";
import initStripe from "stripe";
const handler = async (req, res) => {
const { user } = await supabase.auth.api.getUserByCookie(req);
if (!user) {
return res.status(401).send("Unathorized");
}
const token = cookie.parse(req.headers.cookie)["sb-access-token"];
supabase.auth.session = () => ({
access_token: token,
});`
const {
data: { stripe_customer },
} = await supabase
.from("profile")
.select("stripe_customer")
.eq("id", user.id)
.single();
For anyone who tries to figure out how to get the user server side with the new #supabase/auth-helpers-nextjs, Michele gave the answer.
Just a note: If you're trying to get the user on nextJs's Middleware, instead of:
... req.cookies["sb-access-token"]
You have to use: req.cookies.get('sb-access-token')
For example:
import { supabaseServerClient } from '#supabase/auth-helpers-nextjs';
const { user } = await supabaseServerClient({ req, res }).auth.api.getUser(req.cookies.get('sb-access-token'))
UPDATE: 2023. Available now on Supabase Docs here
import { createServerSupabaseClient } from '#supabase/auth-helpers-nextjs'
export default function Profile({ user }) {
return <div>Hello {user.name}</div>
}
export const getServerSideProps = async (ctx) => {
// Create authenticated Supabase Client
const supabase = createServerSupabaseClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
return {
props: {
initialSession: session,
user: session.user,
},
}
}

How to refresh app when opened from deeplink?

I am struggling with a react native app. I would implement react native firebase dynamic link, but now I am a little lost. I use this method on HomeScreen which working perfectly every times when somebody opens the app.
async componentWillMount() {
try {
let url = await firebase.links().getInitialLink();
if(url) {
let api = "example.com/user/123456";
try {
this.setState({ data: "John Doe" });
this.props.navigation.navigate('Preview', {user: this.state.data })
}
catch {
}
}
}
catch {
}
}
But when the app is already opened this method doesn't work properly. Is there a way where I can trigger a function every time when somebody comes back to the opened app?
Just a tip, you should place your code in componentDidMount so that you do not block the initial (first) render.
You could use AppState to listen out for changes to apps being put in the background/foreground.
componentDidMount() {
this.showPreview();
AppState.addEventListener('change', this.onAppStateChange);
}
componentWillUnmount() {
AppState.removeEventListener('change', this.onAppStateChange);
}
const onAppStateChange = appState => {
// You can check if appState is active/background/foreground
this.showPreview();
}
const showPreview = async (appState) => {
// You can check if appState is active/inactive/background
try {
let url = await firebase.links().getInitialLink();
if(url) {
let api = "example.com/user/123456";
try {
this.setState({ data: "John Doe" });
this.props.navigation.navigate('Preview', {user: this.state.data })
}
catch {
}
}
}
catch(e) {
console.error(e);
}
}

Duplicates in offline Meteor/React Native app

I usually find a solution to a problem with Google/Stackoverflow/Meteor forums or find someone to fix it. However, this time I’m completely stuck. Here is my issue:
I’ve created a field service management app with Meteor/React/React Native. Managers can create tasks/work orders on the web app, the field team can create reports/work logs on the mobile app.
However, the region I’m in has a poor internet connection in some areas. So the offline feature is essential for both viewing tasks/reports but also creating the reports (not the tasks). To solve the offline data access, I’ve followed Spencer Carli’s excellent tutorial https://hackernoon.com/offline-first-react-native-meteor-apps-2bee8e976ec7
It’s working very well.
Things go wrong for creating offline reports. Basically, I have created an action queue in redux store to handle offline reports creation. When connection is back online, actions are being mapped, reports are created on the server, actions are then deleted, and offline created reports are being deleted from mini mongo and redux because anyway, once created on the server, it’s auto synced again.
It’s working very well BUT sometimes, especially when internet connection is slow, duplicates are created. And like, 50+ duplicates of the same report sometimes.
Here is the action queue syncing:
async handleSync(props) {
const data = Meteor.getData();
const db = data && data.db;
if (props.loading === false && props.connectionStatus === 'connected' && Meteor.userId() && db && props.actionQueue && props.actionQueue.length > 0) {
for (let action of props.actionQueue) {
if (action.msg === 'createReport') {
const report = {
organizationId: action.doc.organizationId,
taskId: action.doc.taskId,
status: action.doc.status,
startedAt: action.doc.startedAt,
};
const result = await Meteor.call('Reports.create', report, (error, res) => {
if (error) {
Alert.alert(I18n.t('main.error'), `${I18n.t('main.errorSyncReport')} ${error.reason}`);
} else {
props.dispatch({ type: 'REMOVE_ACTION_QUEUE', payload: action.actionId });
props.dispatch({ type: 'REMOVE_OFFLINE_REPORT', payload: action.doc._id });
db['reports'].del(action.doc._id);
const task = {
organizationId: action.doc.organizationId,
taskId: action.doc.taskId,
};
Meteor.call('Tasks.updateTaskStatus', task);
}
});
return result;
}
else if (action.msg === 'completeReport') {
// this action is for completion of reports that have been created online
const report = {
organizationId: action.doc.organizationId,
reportId: action.doc._id,
comments: action.doc.comments,
isTaskCompleted: action.doc.isTaskCompleted,
completedAt: action.doc.completedAt,
fields: action.doc.fields,
};
const result = await Meteor.call('Reports.completeReport', report, (error, res) => {
if (error) {
Alert.alert(I18n.t('main.error'), `${I18n.t('main.errorSyncReport')} ${error.reason}`);
} else {
props.dispatch({ type: 'REMOVE_ACTION_QUEUE', payload: action.actionId });
const task = {
organizationId: action.doc.organizationId,
taskId: action.doc.taskId,
};
Meteor.call('Tasks.updateTaskStatus', task);
}
});
return result;
}
else if (action.msg === 'createCompletedReport') {
// this action is for completion of reports that have been created offline to avoid _id problems
// so a new completed report is created and the offline report is deleted
const report = {
organizationId: action.doc.organizationId,
taskId: action.doc.taskId,
comments: action.doc.comments,
isTaskCompleted: action.doc.isTaskCompleted,
fields: action.doc.fields,
status: action.doc.status,
startedAt: action.doc.startedAt,
completedAt: action.doc.completedAt,
};
const result = await Meteor.call('Reports.create', report, (error, res) => {
if (error) {
Alert.alert(I18n.t('main.error'), `${I18n.t('main.errorSyncReport')} ${error.reason}`);
} else {
props.dispatch({ type: 'REMOVE_ACTION_QUEUE', payload: action.actionId });
props.dispatch({ type: 'REMOVE_OFFLINE_REPORT', payload: action.doc._id });
db['reports'].del(action.doc._id);
const task = {
organizationId: action.doc.organizationId,
taskId: action.doc.taskId,
};
Meteor.call('Tasks.updateTaskStatus', task);
}
});
return result;
}
}
}
}
Here is the offline initialisation based on Spencer’s tutorial:
const onRehydration = (store) => {
const data = Meteor.getData();
const db = data && data.db;
if (db) {
_.each(store.getState(), (collectionData, collectionName) => {
if (collectionName !== 'offlineUser' && collectionName !== 'offlineOrg' && collectionName !== 'actionQueue' && collectionName !== 'clipboard') {
if (!db[collectionName]) {
db.addCollection(collectionName);
}
const collectionArr = _.map(collectionData, (doc, _id) => {
doc._id = _id;
return doc;
});
db[collectionName].upsert(collectionArr);
}
});
}
store.dispatch({type: 'CACHING', caching: false})
};
export const initializeOffline = (opts = {}) => {
let debug = false;
const logger = createLogger({ predicate: () => debug&&opts.log || false });
const store = createStore(reducers, applyMiddleware(logger), autoRehydrate());
persistStore(store, {
storage: AsyncStorage,
keyPrefix: 'offline:',
debounce: opts.debounce || 2000,
}, () => onRehydration(store));
store.dispatch({type: 'CACHING', caching: true})
Meteor.ddp.on('added', (payload) => {
store.dispatch({ type: 'DDP_ADDED', payload });
});
Meteor.ddp.on('changed', (payload) => {
store.dispatch({ type: 'DDP_CHANGED', payload });
});
Meteor.ddp.on('removed', (payload) => {
store.dispatch({ type: 'DDP_REMOVED', payload });
});
return store;
};
If someone has an idea of the problem or ever encountered a similar issue, I’d be grateful if you could share your solution :)

Resources