-
Notifications
You must be signed in to change notification settings - Fork 13
chore: handle conflict from firebase #227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: beta
Are you sure you want to change the base?
Changes from all commits
1a8f631
74e492c
01e96ac
809208b
06f75e2
6f8a009
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| const fs = require("fs-extra"); | ||
| const path = require("path"); | ||
|
|
||
| const testProjectPath = path.join(__dirname, "../../../test-app"); | ||
| const iosPath = path.join(testProjectPath, "ios"); | ||
| const appName = "ExpoTestbed"; // Replace with your app name if different | ||
| // Define all possible locations where GoogleService-Info.plist might exist | ||
| const possibleLocations = [ | ||
| path.join(iosPath, "GoogleService-Info.plist"), // Our plugin's default location | ||
| path.join(iosPath, appName, "GoogleService-Info.plist"), // Where React Native Firebase typically adds it | ||
| ]; | ||
|
|
||
| describe("GoogleService-Info.plist File", () => { | ||
| test("GoogleService-Info.plist exists in at least one expected location", () => { | ||
| // Check all possible locations | ||
| const existsInAnyLocation = possibleLocations.some(location => fs.existsSync(location)); | ||
|
|
||
| // Log all locations where the file exists | ||
| const existingLocations = possibleLocations.filter(location => fs.existsSync(location)); | ||
| if (existingLocations.length > 0) { | ||
| console.log("GoogleService-Info.plist found in these locations:"); | ||
| existingLocations.forEach(location => console.log(`- ${location}`)); | ||
| } else { | ||
| console.log("GoogleService-Info.plist not found in any expected location"); | ||
| } | ||
|
|
||
| expect(existsInAnyLocation).toBe(true); | ||
| }); | ||
|
|
||
| test("GoogleService-Info.plist file is not duplicated", () => { | ||
| // Count how many locations have the file | ||
| const existingLocations = possibleLocations.filter(location => fs.existsSync(location)); | ||
|
|
||
| // If more than one location has the file, log a warning | ||
| if (existingLocations.length > 1) { | ||
| console.warn("WARNING: GoogleService-Info.plist found in multiple locations:"); | ||
| existingLocations.forEach(location => console.log(`- ${location}`)); | ||
| } | ||
|
|
||
| // We expect the file to be in at most one location | ||
| // This test will pass if the file exists in exactly one location | ||
| // and will fail if it's duplicated | ||
| expect(existingLocations.length).toBeLessThanOrEqual(1); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,68 +1,232 @@ | ||
| import { withXcodeProject, IOSConfig, ConfigPlugin } from '@expo/config-plugins'; | ||
| import { | ||
| withXcodeProject, | ||
| IOSConfig, | ||
| ConfigPlugin, | ||
| } from '@expo/config-plugins'; | ||
|
|
||
| import { FileManagement } from './../helpers/utils/fileManagement'; | ||
| import type { CustomerIOPluginOptionsIOS } from '../types/cio-types'; | ||
| import { isFcmPushProvider } from './utils'; | ||
|
|
||
| export const withGoogleServicesJsonFile: ConfigPlugin<CustomerIOPluginOptionsIOS> = ( | ||
| config, | ||
| cioProps | ||
| ) => { | ||
| return withXcodeProject(config, async (props) => { | ||
| /** | ||
| * Checks if a file is already referenced in the Xcode project | ||
| * | ||
| * @param project The Xcode project | ||
| * @param fileName The file name to check | ||
| * @returns True if the file is already referenced in the project | ||
| */ | ||
| function isFileReferencedInXcodeProject(project: any, fileName: string): boolean { | ||
| try { | ||
| // Get all file references | ||
| const fileReferences = project.pbxFileReferenceSection(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have considered this option initially but I was hesitant to do that route since I don't know how reliable it is to be parsing Xcode project file and if this might fail with Xcode upgrades or things like that? Specially since there are no Expo plugins or mods that allow us to access those details in a structured way. Or maybe even this file looks different for complicated project structures, at the end of the day, the vast majority of iOS development depend on Xcode updating this file when files are added to the project and it's not done manually
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have found the xdode package to be very reliable, the approach is actually the most common way to programmatically interact with Xcode projects in the React Native ecosystem. Most tools (including React Native itself in the CLI), fastlane, expo config use similar methods. While Apple does change the Xcode project format in xcode upgrades, they generally maintain backward compatibility, especially for core features like file references. And lets say there is a case with very complicated structure out of ordinary, and this check fails, the consequences would be we get a duplicated reference which we were going to get anyway? and if someone has this issue we can suggest the solution of not adding the googleservices file in our config?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've recently updated our Xcode project file for APN UIKit to the newest version (available with Xcode 16.x), and our existing project file parsing (in .github workflow/actions) failed. Reference:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually think we should re-think this change, because a similar logic was problematic with Expo warnings for version conflicts. I believe this is could even be more problematic. Like @uros-mil mentions, seems like Xcode 16 added/changed this file format and we don't wanna keep testing and supporting multiple Xcode project files format
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't analyze this in depth, but my logic is:
|
||
|
|
||
| const pushProvider = cioProps.pushNotification?.provider ?? 'apn'; | ||
| const useFcm = pushProvider === 'fcm'; | ||
| if (!useFcm) { | ||
| // Nothing to do, for providers other than FCM, the Google services JSON file isn't needed | ||
| return props; | ||
| // Check if any file reference matches our fileName | ||
| for (const key in fileReferences) { | ||
| const fileReference = fileReferences[key]; | ||
| if (typeof fileReference === 'object' && fileReference.name === fileName) { | ||
| return true; | ||
| } | ||
|
|
||
| // Some file references might use path instead of name | ||
| if (typeof fileReference === 'object' && fileReference.path === fileName) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| // googleServicesFile | ||
| const iosPath = props.modRequest.platformProjectRoot; | ||
| const googleServicesFile = cioProps.pushNotification?.googleServicesFile; | ||
| if (!FileManagement.exists(`${iosPath}/GoogleService-Info.plist`)) { | ||
| if (googleServicesFile && FileManagement.exists(googleServicesFile)) { | ||
| try { | ||
| FileManagement.copyFile( | ||
| googleServicesFile, | ||
| `${iosPath}/GoogleService-Info.plist` | ||
| ); | ||
|
|
||
| addFileToXcodeProject(props.modResults, "GoogleService-Info.plist"); | ||
| } catch (e) { | ||
| console.error( | ||
| `There was an error copying your GoogleService-Info.plist file. You can copy it manually into ${iosPath}/GoogleService-Info.plist` | ||
| ); | ||
|
|
||
| // Check in resources build phase as well | ||
| const buildPhases = project.pbxResourcesBuildPhaseSection(); | ||
| for (const key in buildPhases) { | ||
| const buildPhase = buildPhases[key]; | ||
|
|
||
| if (typeof buildPhase === 'object' && buildPhase.files) { | ||
| for (const fileKey of buildPhase.files) { | ||
| const buildFile = project.pbxBuildFileSection()[fileKey.value]; | ||
| if (buildFile && buildFile.fileRef) { | ||
| const fileRef = project.pbxFileReferenceSection()[buildFile.fileRef]; | ||
| if (fileRef && (fileRef.name === fileName || fileRef.path === fileName)) { | ||
| return true; | ||
| } | ||
| } else { | ||
| console.error( | ||
| `The Google Services file provided in ${googleServicesFile} doesn't seem to exist. You can copy it manually into ${iosPath}/GoogleService-Info.plist` | ||
| ); | ||
| } | ||
| } else { | ||
| console.log( | ||
| `File already exists: ${iosPath}/GoogleService-Info.plist. Skipping...` | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } catch (error) { | ||
| console.warn(`Error checking if ${fileName} is referenced in Xcode project: ${error}`); | ||
| // In case of error, assume it's not referenced to be safe | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return props; | ||
| }); | ||
| }; | ||
| /** | ||
| * Adds a file to the Xcode project | ||
| * | ||
| * @param project The Xcode project | ||
| * @param fileName The file name to add | ||
| * @returns boolean indicating success | ||
| */ | ||
| function addFileToXcodeProject(project: any, fileName: string): boolean { | ||
| const groupName = 'Resources'; | ||
| const filepath = fileName; | ||
|
|
||
| function addFileToXcodeProject(project: any, fileName: string) { | ||
| const groupName = "Resources"; | ||
| const filepath = fileName; | ||
|
|
||
| if (!IOSConfig.XcodeUtils.ensureGroupRecursively(project, groupName)) { | ||
| console.error(`Error copying GoogleService-Info.plist. Failed to find or create '${groupName}' group in Xcode.`); | ||
| return; | ||
| } | ||
|
|
||
| if (!IOSConfig.XcodeUtils.ensureGroupRecursively(project, groupName)) { | ||
| console.error( | ||
| `Error copying GoogleService-Info.plist. Failed to find or create '${groupName}' group in Xcode.` | ||
| ); | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| // Add GoogleService-Info.plist to the Xcode project | ||
| IOSConfig.XcodeUtils.addResourceFileToGroup({ | ||
| project, | ||
| filepath, | ||
| groupName, | ||
| isBuildFile: true, | ||
| }); | ||
| } | ||
| return true; | ||
| } catch (error) { | ||
| // Handle potential errors like file already added | ||
| if (String(error).includes('already exists')) { | ||
| console.log(`File ${fileName} is already in the Xcode project. Skipping addition.`); | ||
| return true; | ||
| } else { | ||
| console.error(`Error adding ${fileName} to Xcode project: ${error}`); | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Find an existing GoogleService-Info.plist file in common locations | ||
| * | ||
| * @param iosPath iOS project root path | ||
| * @param appName iOS app name | ||
| * @returns Path to existing file or null if not found | ||
| */ | ||
| function findExistingGoogleServicesFile(iosPath: string, appName: string): string | null { | ||
| // Define all possible locations where GoogleService-Info.plist might exist | ||
| const possibleLocations = [ | ||
| `${iosPath}/GoogleService-Info.plist`, // Our plugin's default location | ||
| `${iosPath}/${appName}/GoogleService-Info.plist` // Where React Native Firebase typically adds it | ||
| ]; | ||
|
|
||
| for (const location of possibleLocations) { | ||
| if (FileManagement.exists(location)) { | ||
| console.log(`Found existing GoogleService-Info.plist at ${location}`); | ||
| return location; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Checks for configuration conflicts between Expo and CustomerIO | ||
| * | ||
| * @param config Expo config | ||
| * @param googleServicesFile Customer IO googleServicesFile path | ||
| */ | ||
| function checkConfigConflicts(config: any, googleServicesFile: string | undefined): void { | ||
| if (config.ios?.googleServicesFile && googleServicesFile) { | ||
| console.warn( | ||
| 'CONFLICT DETECTED: Specifying both Expo ios.googleServicesFile and Customer IO ios.pushNotification.googleServicesFile' + | ||
| ' will cause a conflict by duplicating GoogleService-Info.plist in the iOS project resources.' + | ||
| '\nRECOMMENDATION: Please remove Customer IO ios.pushNotification.googleServicesFile from your configuration.' | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Copy GoogleService-Info.plist from source to destination and add to Xcode project | ||
| * | ||
| * @param sourceFile Source file path | ||
| * @param destinationPath Destination path | ||
| * @param project Xcode project | ||
| * @returns boolean indicating success | ||
| */ | ||
| function copyAndAddGoogleServicesFile( | ||
| sourceFile: string, | ||
| destinationPath: string, | ||
| project: any | ||
| ): boolean { | ||
| try { | ||
| console.log(`Copying GoogleService-Info.plist from ${sourceFile} to ${destinationPath}`); | ||
| FileManagement.copyFile(sourceFile, destinationPath); | ||
|
|
||
| const success = addFileToXcodeProject(project, 'GoogleService-Info.plist'); | ||
| if (success) { | ||
| console.log('Successfully added GoogleService-Info.plist to Xcode project'); | ||
| } | ||
| return success; | ||
| } catch (e) { | ||
| console.error( | ||
| `ERROR: There was an error copying your GoogleService-Info.plist file: ${e}` + | ||
| `\nYou can copy it manually into ${destinationPath} and add it to your Xcode project` | ||
| ); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| export const withGoogleServicesJsonFile: ConfigPlugin< | ||
| CustomerIOPluginOptionsIOS | ||
| > = (config, cioProps) => { | ||
| return withXcodeProject(config, async (props) => { | ||
| const useFcm = isFcmPushProvider(cioProps); | ||
| if (!useFcm) { | ||
| // Nothing to do, for providers other than FCM, the Google services JSON file isn't needed | ||
| return props; | ||
| } | ||
|
|
||
| console.log( | ||
| 'Only specify Customer IO ios.pushNotification.googleServicesFile config if you are not already including' + | ||
| ' GoogleService-Info.plist as part of Firebase integration' | ||
| ); | ||
|
|
||
| const iosPath = props.modRequest.platformProjectRoot; | ||
| const appName = props.modRequest.projectName; | ||
| const googleServicesFile = cioProps.pushNotification?.googleServicesFile; | ||
| const fileName = 'GoogleService-Info.plist'; | ||
| const destinationPath = `${iosPath}/${fileName}`; | ||
|
|
||
| // Check if file already exists in common locations | ||
| // We know appName should be defined in the context of an Expo plugin | ||
| const existingFilePath = findExistingGoogleServicesFile(iosPath, appName as string); | ||
|
|
||
| if (existingFilePath) { | ||
| console.log(`File already exists: ${existingFilePath}. Skipping copy...`); | ||
|
|
||
| // If the file is in the main iOS directory, check if it's in the Xcode project | ||
| if (existingFilePath === destinationPath && !isFileReferencedInXcodeProject(props.modResults, fileName)) { | ||
| console.log('Adding existing GoogleService-Info.plist to Xcode project...'); | ||
| addFileToXcodeProject(props.modResults, fileName); | ||
| } else { | ||
| console.log('GoogleService-Info.plist is already referenced or in a location handled by other tools. No action needed.'); | ||
| } | ||
|
|
||
| return props; | ||
| } | ||
|
|
||
| // Check for config conflicts | ||
| checkConfigConflicts(config, googleServicesFile); | ||
|
|
||
| // Only copy if the file wasn't found anywhere and a source file was provided | ||
| if (googleServicesFile && FileManagement.exists(googleServicesFile)) { | ||
| copyAndAddGoogleServicesFile(googleServicesFile, destinationPath, props.modResults); | ||
| } else if (googleServicesFile) { | ||
| console.error( | ||
| `ERROR: The Google Services file specified at "${googleServicesFile}" does not exist.` + | ||
| `\nPlease check the path and make sure the file exists, or copy it manually into ${destinationPath}` | ||
| ); | ||
| } else { | ||
| console.warn( | ||
| `WARNING: No GoogleService-Info.plist source file was provided in the configuration.` + | ||
| `\nIf you're using FCM for push notifications, you need to provide the file.` + | ||
| `\nPlease add it manually to ${destinationPath} or configure it through Customer IO ios.pushNotification.googleServicesFile` | ||
| ); | ||
| } | ||
|
|
||
| return props; | ||
| }); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think those tests will never fail since our test project doesn't have any Firebase SDKs setup for it, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes thats correct, created them so when we run these on the branch where you added react native firebase, we can verify it via unit test too.
also, might help even in future when we want to quickly test any other rn-firebase feature while making sure nothing else breaks. But it can be removed if deemed unnecessary.