enh
This commit is contained in:
parent
dc11e77707
commit
637c90a771
15 changed files with 4966 additions and 15 deletions
|
|
@ -35,6 +35,7 @@ const extractKeyFromUrl = (url: string): string => {
|
||||||
export default function SendNotifications() {
|
export default function SendNotifications() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
|
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [selectedImage, setSelectedImage] = useState<{ blob: Blob; mimeType: string } | null>(null);
|
const [selectedImage, setSelectedImage] = useState<{ blob: Blob; mimeType: string } | null>(null);
|
||||||
const [displayImage, setDisplayImage] = useState<{ uri?: string } | null>(null);
|
const [displayImage, setDisplayImage] = useState<{ uri?: string } | null>(null);
|
||||||
|
|
@ -54,6 +55,7 @@ export default function SendNotifications() {
|
||||||
Alert.alert('Success', 'Notification sent successfully!');
|
Alert.alert('Success', 'Notification sent successfully!');
|
||||||
// Reset form
|
// Reset form
|
||||||
setSelectedUserIds([]);
|
setSelectedUserIds([]);
|
||||||
|
setTitle('');
|
||||||
setMessage('');
|
setMessage('');
|
||||||
setSelectedImage(null);
|
setSelectedImage(null);
|
||||||
setDisplayImage(null);
|
setDisplayImage(null);
|
||||||
|
|
@ -94,6 +96,11 @@ export default function SendNotifications() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
|
if (title.trim().length === 0) {
|
||||||
|
Alert.alert('Error', 'Please enter a title');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.trim().length === 0) {
|
if (message.trim().length === 0) {
|
||||||
Alert.alert('Error', 'Please enter a message');
|
Alert.alert('Error', 'Please enter a message');
|
||||||
return;
|
return;
|
||||||
|
|
@ -136,6 +143,7 @@ export default function SendNotifications() {
|
||||||
// Send notification
|
// Send notification
|
||||||
await sendNotification.mutateAsync({
|
await sendNotification.mutateAsync({
|
||||||
userIds: selectedUserIds,
|
userIds: selectedUserIds,
|
||||||
|
title: title.trim(),
|
||||||
text: message.trim(),
|
text: message.trim(),
|
||||||
imageUrl,
|
imageUrl,
|
||||||
});
|
});
|
||||||
|
|
@ -196,6 +204,17 @@ export default function SendNotifications() {
|
||||||
</MyText>
|
</MyText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Title Input */}
|
||||||
|
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
||||||
|
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Title</MyText>
|
||||||
|
<MyTextInput
|
||||||
|
value={title}
|
||||||
|
onChangeText={setTitle}
|
||||||
|
placeholder="Enter notification title..."
|
||||||
|
style={tw`text-gray-900`}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Message Input */}
|
{/* Message Input */}
|
||||||
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
||||||
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Message</MyText>
|
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Message</MyText>
|
||||||
|
|
@ -223,9 +242,9 @@ export default function SendNotifications() {
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handleSend}
|
onPress={handleSend}
|
||||||
disabled={sendNotification.isPending || message.trim().length === 0 || selectedUserIds.length === 0}
|
disabled={sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0 || selectedUserIds.length === 0}
|
||||||
style={tw`${
|
style={tw`${
|
||||||
sendNotification.isPending || message.trim().length === 0 || selectedUserIds.length === 0
|
sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0 || selectedUserIds.length === 0
|
||||||
? 'bg-gray-300'
|
? 'bg-gray-300'
|
||||||
: 'bg-blue-600'
|
: 'bg-blue-600'
|
||||||
} rounded-xl py-4 items-center shadow-sm`}
|
} rounded-xl py-4 items-center shadow-sm`}
|
||||||
|
|
|
||||||
13
apps/backend/assets/fcm-v1-account.json
Normal file
13
apps/backend/assets/fcm-v1-account.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "freshyo-cefb2",
|
||||||
|
"private_key_id": "dcdb3d9edb6505567db69bbd24e447df78c82dc7",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDE3TSKEL9CF7yP\nUiSIvQC024yQGrERz1wtErH5Xff4pie1LSL1pXJHTpyWojp6dotkmCpxM36XjS+O\nO5pnJhSVwiYgvSxeO3EL6oNQwLP36pxQ7YwmoaFx9Jipau+OK+VY8Y/eMx4cWUJH\n7WeUDGwwJlMKE6CpEsbbBiAY5bF9wwe7v1YlkAnMm5ZZcujCqW1aShWKXuYoUMoP\n6egEiclCdQrHZ5IQCHRWruFTAOBuJ7v0A/9WM1gi7UM3VU3/8ccswP8DDoCrgrmh\nerUhFAEFMEjsns0B8SmwQ8v3GH5/SG0SCDwJniPFPnzdSxksaEB51OTaBcROJlED\nkwZZ+2u/AgMBAAECggEAPYL2vysjb6XWC5w5gSY5Ocmd/orwh+WYYhcE2CuV5zIX\nlyM+2K106zXzdJfFGO3AeVKYdF2IMRdy5AjYomFCLlcHLdSeL+V32abRmCJWOWEr\nrZfD4nA/b0ljiBA7QNuTYnq8HswvHOGA9dOGuTo2dccLzEq8uQd+bgJYdh8TGf2R\nqOpYHdRUJqDl+EuCDqLLqnq4l8E981GN78iVVL4DnYFE/3wb7tmuONww2+grq7ou\ntDtPQf4yNE2Vfx+5JnMsvU+J+iF/4vCI/9Oyg0keW/C8q9rbDdmeyefGRGWHtCCI\nH1wMzYTn2xw7EBH9O1NHDzTWkSTUfeo2dnaR0loVsQKBgQDnuwk180u1QXoDLUzj\ngd86CRnP/zqijdt+Y2oZnT+uHJrHCJbYNt3bdKRUEBU5KEcMdzMaeR8A1YEYK2oD\nd8M42nsOn22VymT0fIqwrHsf9e5mgGV+novqw848aEmgTIEmBnSYKc3Xa3px3wge\nJWLKlX/+y2uvI9En8u1FGQ0wcQKBgQDZe1uLqd6koPh/+VSAx2OtjCDgCAvlYoUh\nwIH3tFab/p41DyR+VDx6z18MACsSmyiewV4xUBmu1o+H/iiOxPXQvf7QchY+fYYb\nzOMJXM4ddcGUdLF8CPapbIFcLKemQIb0PIlrCQQeXq2E74JacP8kdqNmCQ8J/GZF\nMPapRTt3LwKBgQCU5jLJ7tZD1pnO9snEGkxUn0ptw0Nq9hoGwVyIrukfOJQfth4v\nOjoebHm25kqs2nukv+cfaJqKT6ZO4H6TUd4oZwLRZ5HjwRRToL8BPSM0azNPu8r7\nrGadaEnZuO0uSlpmE5nRuHLiq9YW20f9DurG339KOm2sMSiRMeBSGQHHkQKBgGTZ\nFQxgiwOgOVtujMbirtAtKJl6YbnOw5lxIVNx5q+TlF1aVjvWZ+0y+Aoikdag6Gcl\nl74aPK6chBY1vyzlHG/diqmyHaqAno2JpsYSqOl0T3291weDSI4r6JiLhHpNdccP\nw1FE7wn+MUxxm+rAdy+7a+3GyZiB2BLBr7+ygO61AoGBALySZ9m4hgX6uZtJvv3R\nrl8AWoG65NHCZ4694aEGTJDVDlPByV+Sd5iBOQ5dvhgA12Py2uj5ZHQXbuo0IGfJ\ngH8AZMIKX9UrhbE5BWYncg2ZR8uvKow8w36mLNnQhGZ71IZ9MXbWbpEK8CbCEvzZ\nyw0rKVgrrSRihW3stnl16Zs5\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "firebase-adminsdk-fbsvc@freshyo-cefb2.iam.gserviceaccount.com",
|
||||||
|
"client_id": "117456013812283364643",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40freshyo-cefb2.iam.gserviceaccount.com",
|
||||||
|
"universe_domain": "googleapis.com"
|
||||||
|
}
|
||||||
1
apps/backend/drizzle/0073_faithful_gravity.sql
Normal file
1
apps/backend/drizzle/0073_faithful_gravity.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "mf"."user_notifications" ADD COLUMN "title" varchar(255) NOT NULL;
|
||||||
3691
apps/backend/drizzle/meta/0073_snapshot.json
Normal file
3691
apps/backend/drizzle/meta/0073_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -512,6 +512,13 @@
|
||||||
"when": 1770546741428,
|
"when": 1770546741428,
|
||||||
"tag": "0072_flowery_deathbird",
|
"tag": "0072_flowery_deathbird",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 73,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1770561175889,
|
||||||
|
"tag": "0073_faithful_gravity",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
"expo-server-sdk": "^4.0.0",
|
"expo-server-sdk": "^4.0.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"firebase-admin": "^13.6.1",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
|
|
|
||||||
|
|
@ -421,6 +421,7 @@ export const notifCreds = mf.table('notif_creds', {
|
||||||
|
|
||||||
export const userNotifications = mf.table('user_notifications', {
|
export const userNotifications = mf.table('user_notifications', {
|
||||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||||
|
title: varchar('title', { length: 255 }).notNull(),
|
||||||
imageUrl: varchar('image_url', { length: 500 }),
|
imageUrl: varchar('image_url', { length: 500 }),
|
||||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
body: text('body').notNull(),
|
body: text('body').notNull(),
|
||||||
|
|
|
||||||
14
apps/backend/src/lib/firebase.ts
Normal file
14
apps/backend/src/lib/firebase.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import admin from 'firebase-admin';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// Initialize Firebase Admin SDK
|
||||||
|
const serviceAccountPath = path.join(process.cwd(), 'assets', 'fcm-v1-account.json');
|
||||||
|
|
||||||
|
if (!admin.apps.length) {
|
||||||
|
admin.initializeApp({
|
||||||
|
credential: admin.credential.cert(serviceAccountPath),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messaging = admin.messaging();
|
||||||
|
export default admin;
|
||||||
|
|
@ -4,6 +4,14 @@ import { db } from '../../db/db_index';
|
||||||
import { users, complaints, orders, orderItems, notifCreds, userNotifications, userDetails } from '../../db/schema';
|
import { users, complaints, orders, orderItems, notifCreds, userNotifications, userDetails } from '../../db/schema';
|
||||||
import { eq, sql, desc, asc, count, max } from 'drizzle-orm';
|
import { eq, sql, desc, asc, count, max } from 'drizzle-orm';
|
||||||
import { ApiError } from '../../lib/api-error';
|
import { ApiError } from '../../lib/api-error';
|
||||||
|
import { Expo } from 'expo-server-sdk';
|
||||||
|
import { messaging } from '../../lib/firebase';
|
||||||
|
import { generateSignedUrlFromS3Url } from '../../lib/s3-client';
|
||||||
|
|
||||||
|
// Toggle between notification providers: 'expo' | 'fcm'
|
||||||
|
// const NOTIFICATION_PROVIDER: 'expo' | 'fcm' = 'fcm';
|
||||||
|
// let NOTIFICATION_PROVIDER: 'expo' | 'fcm' = 'expo';
|
||||||
|
let NOTIFICATION_PROVIDER: string = 'expo';
|
||||||
|
|
||||||
async function createUserByMobile(mobile: string): Promise<typeof users.$inferSelect> {
|
async function createUserByMobile(mobile: string): Promise<typeof users.$inferSelect> {
|
||||||
// Clean mobile number (remove non-digits)
|
// Clean mobile number (remove non-digits)
|
||||||
|
|
@ -362,24 +370,133 @@ export const userRouter = {
|
||||||
sendNotification: protectedProcedure
|
sendNotification: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
userIds: z.array(z.number()),
|
userIds: z.array(z.number()),
|
||||||
|
title: z.string().min(1, 'Title is required'),
|
||||||
text: z.string().min(1, 'Message is required'),
|
text: z.string().min(1, 'Message is required'),
|
||||||
imageUrl: z.string().optional(),
|
imageUrl: z.string().optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { userIds, text, imageUrl } = input;
|
const { userIds, title, text, imageUrl } = input;
|
||||||
|
|
||||||
// Store notification in database
|
// Store notification in database
|
||||||
await db.insert(userNotifications).values({
|
await db.insert(userNotifications).values({
|
||||||
|
title,
|
||||||
body: text,
|
body: text,
|
||||||
imageUrl: imageUrl || null,
|
imageUrl: imageUrl || null,
|
||||||
applicableUsers: userIds.length > 0 ? userIds : null,
|
applicableUsers: userIds.length > 0 ? userIds : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Implement actual push notification logic
|
// Fetch push tokens for target users
|
||||||
|
const tokens = await db
|
||||||
|
.select({ token: notifCreds.token, userId: notifCreds.userId })
|
||||||
|
.from(notifCreds)
|
||||||
|
.where(sql`${notifCreds.userId} IN (${sql.join(userIds, sql`, `)})`);
|
||||||
|
|
||||||
|
if (tokens.length === 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Notification saved but no push tokens found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate signed URL for image if provided
|
||||||
|
const signedImageUrl = imageUrl ? await generateSignedUrlFromS3Url(imageUrl) : null;
|
||||||
|
|
||||||
|
let sentCount = 0;
|
||||||
|
let failedCount = 0;
|
||||||
|
|
||||||
|
if (NOTIFICATION_PROVIDER === 'fcm') {
|
||||||
|
// Build FCM messages
|
||||||
|
const messages = tokens.map(({ token }) => ({
|
||||||
|
token: token,
|
||||||
|
notification: {
|
||||||
|
title: title,
|
||||||
|
body: text,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
imageUrl: imageUrl || '',
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
notification: {
|
||||||
|
imageUrl: signedImageUrl || undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apns: {
|
||||||
|
payload: {
|
||||||
|
aps: {
|
||||||
|
'mutable-content': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fcm_options: {
|
||||||
|
image: signedImageUrl || undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Send notifications using Firebase Admin SDK
|
||||||
|
try {
|
||||||
|
const response = await messaging.sendEach(messages);
|
||||||
|
sentCount = response.successCount;
|
||||||
|
failedCount = response.failureCount;
|
||||||
|
|
||||||
|
// Log failed tokens
|
||||||
|
response.responses.forEach((resp, idx) => {
|
||||||
|
if (!resp.success) {
|
||||||
|
console.error(`Failed to send to token ${tokens[idx].token}:`, resp.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending push notifications:', error);
|
||||||
|
throw new ApiError('Failed to send push notifications', 500);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Send using Expo (legacy)
|
||||||
|
const expo = new Expo();
|
||||||
|
|
||||||
|
// Helper function to chunk array
|
||||||
|
const chunkArray = (array: any[], size: number) => {
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < array.length; i += size) {
|
||||||
|
chunks.push(array.slice(i, i + size));
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
};
|
||||||
|
|
||||||
|
const chunks = chunkArray(tokens, 50);
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
const messages = chunk
|
||||||
|
.filter(({ token }) => Expo.isExpoPushToken(token))
|
||||||
|
.map(({ token }) => ({
|
||||||
|
to: token,
|
||||||
|
sound: 'default',
|
||||||
|
title,
|
||||||
|
body: text,
|
||||||
|
data: { imageUrl },
|
||||||
|
...(signedImageUrl ? {
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
url: signedImageUrl,
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} : {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (messages.length > 0) {
|
||||||
|
try {
|
||||||
|
await expo.sendPushNotificationsAsync(messages);
|
||||||
|
sentCount += messages.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending push notifications:', error);
|
||||||
|
failedCount += chunk.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Notification sent to ${userIds.length > 0 ? userIds.length + ' users' : 'all users'}`,
|
message: `Notification sent to ${sentCount} users${failedCount > 0 ? `, ${failedCount} failed` : ''}`,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
@ -6,7 +6,7 @@ import { ApiError } from '../../lib/api-error';
|
||||||
export const fileUploadRouter = router({
|
export const fileUploadRouter = router({
|
||||||
generateUploadUrls: protectedProcedure
|
generateUploadUrls: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
contextString: z.enum(['review', 'product_info']),
|
contextString: z.enum(['review', 'product_info', 'notification']),
|
||||||
mimeTypes: z.array(z.string()),
|
mimeTypes: z.array(z.string()),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
||||||
|
|
@ -24,6 +24,8 @@ export const fileUploadRouter = router({
|
||||||
folder = 'product-images';
|
folder = 'product-images';
|
||||||
} else if(contextString === 'review_response') {
|
} else if(contextString === 'review_response') {
|
||||||
folder = 'review-response-images'
|
folder = 'review-response-images'
|
||||||
|
} else if(contextString === 'notification') {
|
||||||
|
folder = 'notification-images'
|
||||||
} else {
|
} else {
|
||||||
folder = '';
|
folder = '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Freshyo",
|
"name": "Freshyo",
|
||||||
"slug": "freshyo",
|
"slug": "freshyo",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/freshyo-logo.png",
|
"icon": "./assets/images/freshyo-logo.png",
|
||||||
"scheme": "freshyo",
|
"scheme": "freshyo",
|
||||||
|
|
|
||||||
|
|
@ -355,7 +355,7 @@ export default function Dashboard() {
|
||||||
<MyText
|
<MyText
|
||||||
style={tw`text-xl font-extrabold text-white tracking-tight drop-shadow-md text-neutral-800`}
|
style={tw`text-xl font-extrabold text-white tracking-tight drop-shadow-md text-neutral-800`}
|
||||||
>
|
>
|
||||||
Our Storess
|
Our Stores
|
||||||
</MyText>
|
</MyText>
|
||||||
<MyText style={tw`text-xs font-medium mt-0.5 text-neutral-800`}>
|
<MyText style={tw`text-xs font-medium mt-0.5 text-neutral-800`}>
|
||||||
Fresh from our locations
|
Fresh from our locations
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ export async function registerForPushNotificationsAsync() {
|
||||||
projectId,
|
projectId,
|
||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
return pushTokenString;
|
// const pushTokenString = await Notifications.getDevicePushTokenAsync();
|
||||||
|
// return pushTokenString.data;
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw new Error(`${e}`);
|
throw new Error(`${e}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1095
package-lock.json
generated
1095
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -39,6 +39,7 @@
|
||||||
"expo": "~53.0.22",
|
"expo": "~53.0.22",
|
||||||
"expo-auth-session": "~6.2.1",
|
"expo-auth-session": "~6.2.1",
|
||||||
"expo-crypto": "~14.1.5",
|
"expo-crypto": "~14.1.5",
|
||||||
|
"expo-server-sdk": "^5.0.0",
|
||||||
"expo-web-browser": "~14.2.0",
|
"expo-web-browser": "~14.2.0",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue