mirror of
https://github.com/urosran/cally.git
synced 2025-07-15 09:45:20 +00:00
sync update
This commit is contained in:
@ -42,6 +42,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
|
|
||||||
const {isSyncing} = useSyncEvents()
|
const {isSyncing} = useSyncEvents()
|
||||||
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
||||||
|
useCalSync()
|
||||||
|
|
||||||
const todaysDate = new Date();
|
const todaysDate = new Date();
|
||||||
|
|
||||||
|
@ -164,19 +164,19 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
}
|
}
|
||||||
}, [user, ready, redirectOverride]);
|
}, [user, ready, redirectOverride]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
const handleNotification = async (notification: Notifications.Notification) => {
|
// const handleNotification = async (notification: Notifications.Notification) => {
|
||||||
const eventId = notification?.request?.content?.data?.eventId;
|
// const eventId = notification?.request?.content?.data?.eventId;
|
||||||
|
//
|
||||||
// if (eventId) {
|
// // if (eventId) {
|
||||||
queryClient.invalidateQueries(['events']);
|
// queryClient.invalidateQueries(['events']);
|
||||||
// }
|
// // }
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
// const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
||||||
|
//
|
||||||
return () => sub.remove();
|
// return () => sub.remove();
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -15,7 +15,7 @@ let eventCount = 0;
|
|||||||
let pushTokens = [];
|
let pushTokens = [];
|
||||||
|
|
||||||
const GOOGLE_CALENDAR_ID = "primary";
|
const GOOGLE_CALENDAR_ID = "primary";
|
||||||
const CHANNEL_ID = "unique-channel-id";
|
const CHANNEL_ID = "cally-family-calendar";
|
||||||
const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification";
|
const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification";
|
||||||
|
|
||||||
|
|
||||||
@ -25,10 +25,10 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
|||||||
const eventData = snapshot.data();
|
const eventData = snapshot.data();
|
||||||
const { familyId, creatorId, email } = eventData;
|
const { familyId, creatorId, email } = eventData;
|
||||||
|
|
||||||
if (email) {
|
// if (email) {
|
||||||
console.log('Event has an email field. Skipping notification.');
|
// console.log('Event has an email field. Skipping notification.');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!familyId || !creatorId) {
|
if (!familyId || !creatorId) {
|
||||||
console.error('Missing familyId or creatorId in event data');
|
console.error('Missing familyId or creatorId in event data');
|
||||||
@ -199,8 +199,8 @@ exports.refreshTokens = functions.pubsub.schedule('every 45 minutes').onRun(asyn
|
|||||||
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
|
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
|
||||||
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
|
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
|
||||||
if (googleToken) {
|
if (googleToken) {
|
||||||
const refreshedGoogleToken = await refreshGoogleToken(googleToken);
|
const {refreshedGoogleToken, refreshedRefreshToken} = await refreshGoogleToken(googleToken);
|
||||||
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
|
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: {accessToken: refreshedGoogleToken, refreshToken: refreshedRefreshToken}};
|
||||||
await profileDoc.ref.update({googleAccounts: updatedGoogleAccounts});
|
await profileDoc.ref.update({googleAccounts: updatedGoogleAccounts});
|
||||||
console.log(`Google token updated for user ${profileDoc.id}`);
|
console.log(`Google token updated for user ${profileDoc.id}`);
|
||||||
}
|
}
|
||||||
@ -244,21 +244,6 @@ exports.refreshTokens = functions.pubsub.schedule('every 45 minutes').onRun(asyn
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function refreshGoogleToken(refreshToken) {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('https://oauth2.googleapis.com/token', {
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data.access_token; // Return the new access token
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error refreshing Google token:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshMicrosoftToken(refreshToken) {
|
async function refreshMicrosoftToken(refreshToken) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||||
@ -311,51 +296,109 @@ async function removeInvalidPushToken(pushToken) {
|
|||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
|
// Function to refresh Google Token with additional logging
|
||||||
async function refreshGoogleToken(refreshToken) {
|
async function refreshGoogleToken(refreshToken) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("https://oauth2.googleapis.com/token", {
|
console.log("Refreshing Google token...");
|
||||||
|
const response = await fetch("https://oauth2.googleapis.com/token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
return response.data.access_token;
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error("Error refreshing Google token:", errorData);
|
||||||
|
throw new Error(`Failed to refresh Google token: ${errorData.error || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Google token refreshed successfully");
|
||||||
|
|
||||||
|
// Return both the access token and refresh token (if a new one is provided)
|
||||||
|
return {
|
||||||
|
refreshedGoogleToken: data.access_token,
|
||||||
|
refreshedRefreshToken: data.refresh_token || refreshToken, // Return the existing refresh token if a new one is not provided
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error refreshing Google token:", error.message);
|
console.error("Error refreshing Google token:", error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get Google access tokens for all users and refresh them if needed
|
// Helper function to get Google access tokens for all users and refresh them if needed with logging
|
||||||
async function getGoogleAccessTokens() {
|
async function getGoogleAccessTokens() {
|
||||||
|
console.log("Fetching Google access tokens for all users...");
|
||||||
const tokens = {};
|
const tokens = {};
|
||||||
const profilesSnapshot = await db.collection("Profiles").get();
|
const profilesSnapshot = await db.collection("Profiles").get();
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
profilesSnapshot.docs.map(async (doc) => {
|
profilesSnapshot.docs.map(async (doc) => {
|
||||||
const profileData = doc.data();
|
const profileData = doc.data();
|
||||||
const googleAccounts = profileData.googleAccounts || {};
|
const googleAccounts = profileData?.googleAccounts || {};
|
||||||
|
|
||||||
for (const googleEmail of Object.keys(googleAccounts)) {
|
for (const googleEmail of Object.keys(googleAccounts)) {
|
||||||
const { refreshToken } = googleAccounts[googleEmail];
|
// Check if the googleAccount entry exists and has a refreshToken
|
||||||
|
const accountInfo = googleAccounts[googleEmail];
|
||||||
|
const refreshToken = accountInfo?.refreshToken;
|
||||||
|
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
try {
|
try {
|
||||||
const accessToken = await refreshGoogleToken(refreshToken);
|
console.log(`Refreshing token for user ${doc.id} (email: ${googleEmail})`);
|
||||||
|
const {refreshedGoogleToken} = await refreshGoogleToken(refreshToken);
|
||||||
tokens[doc.id] = accessToken;
|
tokens[doc.id] = accessToken;
|
||||||
|
console.log(`Token refreshed successfully for user ${doc.id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
tokens[doc.id] = accountInfo?.accessToken;
|
||||||
console.error(`Failed to refresh token for user ${doc.id}:`, error.message);
|
console.error(`Failed to refresh token for user ${doc.id}:`, error.message);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`No refresh token available for user ${doc.id} (email: ${googleEmail})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("Access tokens fetched and refreshed as needed");
|
||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to watch Google Calendar events
|
// Function to watch Google Calendar events with additional logging
|
||||||
const watchCalendarEvents = async (userId, token) => {
|
const watchCalendarEvents = async (userId, token) => {
|
||||||
const url = `https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events/watch`;
|
const url = `https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events/watch`;
|
||||||
|
|
||||||
|
// Verify the token is valid
|
||||||
|
console.log(`Attempting to watch calendar for user ${userId}`);
|
||||||
|
console.log(`Token being used: ${token ? 'present' : 'missing'}`);
|
||||||
|
console.log(`Calendar ID: ${GOOGLE_CALENDAR_ID}`);
|
||||||
|
console.log(`Webhook URL: ${WEBHOOK_URL}?userId=${userId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test the token first
|
||||||
|
const testResponse = await fetch(
|
||||||
|
`https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events?maxResults=1`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!testResponse.ok) {
|
||||||
|
console.error(`Token validation failed for user ${userId}:`, await testResponse.text());
|
||||||
|
throw new Error('Token validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Token validated successfully for user ${userId}`);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -363,50 +406,95 @@ const watchCalendarEvents = async (userId, token) => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: `${CHANNEL_ID}-${userId}`, // Unique ID per user
|
id: `${CHANNEL_ID}-${userId}`,
|
||||||
type: "web_hook",
|
type: "web_hook",
|
||||||
address: `${WEBHOOK_URL}?userId=${userId}`, // Pass userId to identify notifications
|
address: `${WEBHOOK_URL}?userId=${userId}`,
|
||||||
params: {
|
params: {
|
||||||
ttl: "80000", // Set to 20 hours in seconds
|
ttl: "80000",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
console.log(`Watch response for user ${userId}:`, responseText);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
console.error(`Failed to watch calendar for user ${userId}:`, responseText);
|
||||||
throw new Error(`Failed to watch calendar: ${errorData.error?.message || response.statusText}`);
|
throw new Error(`Failed to watch calendar: ${responseText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
const result = JSON.parse(responseText);
|
||||||
|
console.log(`Successfully set up Google Calendar watch for user ${userId}`, result);
|
||||||
|
|
||||||
|
// Store the watch details in Firestore for monitoring
|
||||||
|
await db.collection('CalendarWatches').doc(userId).set({
|
||||||
|
watchId: result.id,
|
||||||
|
resourceId: result.resourceId,
|
||||||
|
expiration: result.expiration,
|
||||||
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in watchCalendarEvents for user ${userId}:`, error);
|
||||||
|
// Store the error in Firestore for monitoring
|
||||||
|
await db.collection('CalendarWatchErrors').add({
|
||||||
|
userId,
|
||||||
|
error: error.message,
|
||||||
|
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Schedule function to renew Google Calendar watch every 20 hours for each user
|
// Add this to test webhook connectivity
|
||||||
exports.renewGoogleCalendarWatch = functions.pubsub.schedule("every 20 hours").onRun(async (context) => {
|
exports.testWebhook = functions.https.onRequest(async (req, res) => {
|
||||||
|
console.log('Test webhook received');
|
||||||
|
console.log('Headers:', req.headers);
|
||||||
|
console.log('Body:', req.body);
|
||||||
|
console.log('Query:', req.query);
|
||||||
|
|
||||||
|
res.status(200).send('Test webhook received successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schedule function to renew Google Calendar watch every 20 hours for each user with logging
|
||||||
|
exports.renewGoogleCalendarWatch = functions.pubsub.schedule("every 10 minutes").onRun(async (context) => {
|
||||||
|
console.log("Starting Google Calendar watch renewal process...");
|
||||||
try {
|
try {
|
||||||
const tokens = await getGoogleAccessTokens();
|
const tokens = await getGoogleAccessTokens();
|
||||||
|
console.log("Tokens: ", tokens);
|
||||||
|
|
||||||
for (const [userId, token] of Object.entries(tokens)) {
|
for (const [userId, token] of Object.entries(tokens)) {
|
||||||
try {
|
try {
|
||||||
const result = await watchCalendarEvents(userId, token);
|
await watchCalendarEvents(userId, token);
|
||||||
console.log(`Successfully renewed Google Calendar watch for user ${userId}:`, result);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error renewing Google Calendar watch for user ${userId}:`, error.message);
|
console.error(`Error renewing Google Calendar watch for user ${userId}:`, error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Google Calendar watch renewal process completed");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in renewGoogleCalendarWatch function:", error.message);
|
console.error("Error in renewGoogleCalendarWatch function:", error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Function to handle notifications from Google Calendar for a specific user
|
// Function to handle notifications from Google Calendar with additional logging
|
||||||
exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||||
const userId = req.query.userId; // Extract userId from query params
|
const userId = req.query.userId; // Extract userId from query params
|
||||||
const calendarId = req.body.resourceId;
|
const calendarId = req.body.resourceId;
|
||||||
|
|
||||||
|
console.log(`Received notification for user ${userId} with calendar ID ${calendarId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
// Fetch push tokens for the specific user
|
// Fetch push tokens for the specific user
|
||||||
const userDoc = await db.collection("Profiles").doc(userId).get();
|
const userDoc = await db.collection("Profiles").doc(userId).get();
|
||||||
const userData = userDoc.data();
|
const userData = userDoc.data();
|
||||||
const pushTokens = userData ? userData.pushToken : [];
|
|
||||||
|
// Ensure pushTokens is an array
|
||||||
|
let pushTokens = [];
|
||||||
|
if (userData && userData.pushToken) {
|
||||||
|
pushTokens = Array.isArray(userData.pushToken) ? userData.pushToken : [userData.pushToken];
|
||||||
|
}
|
||||||
|
|
||||||
if (pushTokens.length === 0) {
|
if (pushTokens.length === 0) {
|
||||||
console.log(`No push tokens found for user ${userId}`);
|
console.log(`No push tokens found for user ${userId}`);
|
||||||
@ -454,5 +542,10 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Sync notification sent for user ${userId}`);
|
||||||
res.status(200).send("Sync notification sent.");
|
res.status(200).send("Sync notification sent.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in sendSyncNotification for user ${userId}:`, error.message);
|
||||||
|
res.status(500).send("Failed to send sync notification.");
|
||||||
|
}
|
||||||
});
|
});
|
@ -17,20 +17,23 @@ export const useClearTokens = () => {
|
|||||||
if (provider === "google") {
|
if (provider === "google") {
|
||||||
let googleAccounts = profileData?.googleAccounts;
|
let googleAccounts = profileData?.googleAccounts;
|
||||||
if (googleAccounts) {
|
if (googleAccounts) {
|
||||||
googleAccounts[email] = null;
|
const newGoogleAccounts = {...googleAccounts}
|
||||||
newUserData.googleAccounts = googleAccounts;
|
delete newGoogleAccounts[email];
|
||||||
|
newUserData.googleAccounts = newGoogleAccounts;
|
||||||
}
|
}
|
||||||
} else if (provider === "outlook") {
|
} else if (provider === "outlook") {
|
||||||
let microsoftAccounts = profileData?.microsoftAccounts;
|
let microsoftAccounts = profileData?.microsoftAccounts;
|
||||||
if (microsoftAccounts) {
|
if (microsoftAccounts) {
|
||||||
microsoftAccounts[email] = null;
|
const newMicrosoftAccounts = {...microsoftAccounts}
|
||||||
newUserData.microsoftAccounts = microsoftAccounts;
|
delete microsoftAccounts[email];
|
||||||
|
newUserData.microsoftAccounts = newMicrosoftAccounts;
|
||||||
}
|
}
|
||||||
} else if (provider === "apple") {
|
} else if (provider === "apple") {
|
||||||
let appleAccounts = profileData?.appleAccounts;
|
let appleAccounts = profileData?.appleAccounts;
|
||||||
if (appleAccounts) {
|
if (appleAccounts) {
|
||||||
appleAccounts[email] = null;
|
const newAppleAccounts = {...appleAccounts}
|
||||||
newUserData.appleAccounts = appleAccounts;
|
delete newAppleAccounts[email];
|
||||||
|
newUserData.appleAccounts = newAppleAccounts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await updateUserData({newUserData});
|
await updateUserData({newUserData});
|
||||||
|
@ -82,6 +82,8 @@ export const useCalSync = () => {
|
|||||||
[googleMail]: {accessToken, refreshToken},
|
[googleMail]: {accessToken, refreshToken},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log({refreshToken})
|
||||||
|
|
||||||
await updateUserData({
|
await updateUserData({
|
||||||
newUserData: {googleAccounts: updatedGoogleAccounts},
|
newUserData: {googleAccounts: updatedGoogleAccounts},
|
||||||
});
|
});
|
||||||
@ -243,8 +245,11 @@ export const useCalSync = () => {
|
|||||||
const syncPromises: Promise<void>[] = [];
|
const syncPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
if (profileData?.googleAccounts) {
|
if (profileData?.googleAccounts) {
|
||||||
for (const [email, { accessToken, refreshToken }] of Object.entries(profileData.googleAccounts)) {
|
console.log(profileData.googleAccounts)
|
||||||
syncPromises.push(fetchAndSaveGoogleEvents({ token: accessToken, refreshToken, email }));
|
for (const [email, emailAcc] of Object.entries(profileData.googleAccounts)) {
|
||||||
|
if(emailAcc?.accessToken) {
|
||||||
|
syncPromises.push(fetchAndSaveGoogleEvents({ token: emailAcc?.accessToken, refreshToken: emailAcc?.refreshToken, email }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,7 +309,7 @@ export const useCalSync = () => {
|
|||||||
const eventId = notification?.request?.content?.data?.eventId;
|
const eventId = notification?.request?.content?.data?.eventId;
|
||||||
|
|
||||||
await resyncAllCalendars();
|
await resyncAllCalendars();
|
||||||
queryClient.invalidateQueries(["events"]);
|
// queryClient.invalidateQueries(["events"]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
||||||
|
@ -153,6 +153,7 @@
|
|||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>SplashScreen</string>
|
<string>SplashScreen</string>
|
||||||
|
Reference in New Issue
Block a user