Man, sometimes things are way more difficult than they should be. Exhibit A: “dropping in” Firebase Authentication and Firebase Storage.
I freely admit that my development style is unfortunately guerilla: I’ll go searching for ways to accomplish what I need, add it to my project, and then spend several hours figuring out why it doesn’t work as advertised. Eventually, though, I’ll tease out the threads and get something that at least performs, but which isn’t nice to look at. Then if I have the bandwidth I’ll build a “version 2” which takes the original concept as I understood it and makes it better.
I’ve had a hell of a time getting Auth and Storage roughed into my project, in part because I’m an idiot and in part because I’m also an idiot. After finding a great article that I could use to implement registration, I had to move forward and deal with login after registration, and then turn my sights towards profile updates. That turned out to be unnecessary complex, and when I say unnecessary I mean that it probably would have been easier had I not been involved.
At the top-right of the page is the “Login” button. By default this shows the shadow-icon and text, and has a tasteful drop shadow when moused over. Nice!
Clicking to the login page, we have a few options. If a user has registered via email and password, they can enter that here. If they need to register, I’ve provide a link to the registration page. I also offer three OAuth providers: Google, Twitter, and Facebook. Google works out of the gate, but I had to apply for a developer account for both Twitter and Facebook and of those two, I’ve only implemented Twitter login because Facebook isn’t accepting individual developer projects right now in the hopes of cutting down on bot apps and such from Bad Actors. Ultimately, though, I have to decide if I think my intended audience would be using Facebook as a credentialing service and if not, remove it.
Logging on via OAuth will pop up the familiar auth verification pages, so all the user has to do is select their account and authorize. As this app doesn’t use these services for anything other than pulling a name and portrait, there’s no real permissions to grant.
Portraits and Names
This part was stupidly complex mainly because I had to work with email, Google, and Twitter systems and get them all working in a similar fashion up to a point.
Here’s the thing: logging in with email and password allows a user to set his or her own screen name. Google or Twitter, on the other hand, provides both a name and a portrait. However, a user may not want to use either, and might want to choose a more TTRPG-esque screen name and image for themselves. Thankfully, we can do that. Right now, though, the choice of a screen name is limited to those who register via email, but once the profile management page is set, they’ll be able to handle that for Google and Twitter as well.
Dealing with portraits provided my first opportunity to use React the way nature intended: by creating a mostly stand-alone component that I could use anywhere.
Here’s a (hopefully) quick look at how it went down
There are two states: The “no image” and “with some kind of image”. The “with some kind of image” could either mean the uploaded preview or the currently assigned image. Starting at the start, though, it’s assumed that there is no image, and that we registered with email and password. These are represented in the code as PortraitTarget for the “no image” component, and the PortraitDisplay for the “some kind of image” display. The first component generates the output you see above: a 128×128 pixel dotted square with the limited instructions and button. The second component generates an image with a max size of 256×256 and the “select file” button below it (for changing things up). If the user hovers over the image, they’ll get a tooltip suggesting that they can “click to remove image” and if they click, it resets the component to “no image” status.
These two components, then, are merged into a third component PortraitSelect. Here, if there’s a value for the fileURI property that is passed in, the app will render the image component. Otherwise, it renders the empty selection component. This tactic is used to kind of “pre-chew” the output, so when it comes time to render, the app only has to return PortraitSelect and the logic of which display has already been obfuscated.
The real workhorse of the component is the PortraitBase component. This function is ultimately what is exported from the file, and is done so wrapped in the withFirebase higher-order component. This HOC allows us to access the Firebase API configuration library so that the component can get all of the authenticated user information even though it’s been taken care of several levels up the component tree.
First there are a few state variables that need to be set in order to track the selected file URL and data on the current user. Then the app will have to initialize data using useEffect. This is a “hook”, a kind of meta-method that allows React to subscribe to certain events so that when those events trigger, the app can take action. In this case, the app is listening for any changes in the Firebase authentication state. If a user makes a change to his or her profile, the app will need to know about it so its can update display accordingly. This also needs to happen because while the context passes down the Firebase config from the top level of the app down to the portrait selection component, things don’t fire sequentially; the app may look for user data before it’s finished loading, causing errors and irrational scenarios. Using useEffect allows the app to update itself once it receives word that it has valid user information to display the portrait, or if it ultimately does not, to display the empty selection box.
Then comes the event handlers. First is onFileSelect, which handles the standard HTML “file” input type. The file is parsed for valid types and is then run through a processor library in order to resize the image. Once that’s done, the resulting value is stored in the fileURI state variable and is uploaded to the Firebase Storage bucket.
Next comes onFileRemove. This is a quick handler that will remove all references to custom profile images from the user’s account. Right now, however, it will not reset the image to the user’s default if they logged in with Google or Twitter, so I need to fix that.
Then comes the heavy lifting FileUpload handler. This nests several operations in promises which is a construct that allows for the handling of asynchronous operations. Because sending data to a remote location could take time (network congestion, your connection, etc), the code cannot assume that it has a response on the next line. Instead, it has to wait around until it receives word that the process is finished. It does this via a promise that allows the code to execute once the response has been received.
First the code will pull some user info. Portraits are uploaded to Firebase Storage and use the account’s Firebase Auth-generate user ID, so the site has to collect that info and format a new file name. If the function received a value of TRUE as an argument, then the app has to delete the file from Storage and null out the user’s photoURL property in his or her profile. Otherwise the app will upload the file data, get a URL reference to that image, and then update the user’s photoURL property. At each step if there’s a problem, the app is logging to the development console right now, but later it’ll surface info to the user where relevant, and log everything elsewhere.
Finally, the app returns that original component ProfileSelect. It receives several properties for data and events that are distributed to either the ProfileTarget or ProfileDisplay components (whomever gets to be displayed).
Working with Firebase — once I get the hang of it — is surprisingly easy although getting an image to work with a profile seems a but long-winded.
First, the image is uploaded to Firebase Storage. This is a file repository like Amazon’s S3. Buckets are created to hold files, and subfolders can be made to keep things organized.
Once the file is uploaded and a reference to the public URL has been obtained, the Firebase Auth record for the user has to be updated to look for that image at that endpoint. That means every time the image is updated (with a new image or if the image is deleted) both the Storage and the Auth records need to be modified.
Right now the component is stuck in the homepage for testing, but I need to get to work on a “profile” page which will allow a user to change his or her username, maybe email address, reset passwords, and change their profile picture. Dealing with profiles is always a weird Skinner Box of options. Some do updates in real time; here, the portrait is uploaded as soon as it’s selected, and the user doesn’t need to submit “the page” to get it to take. But other profiles require you to make all the changes on the page and then submit them, all or none. For me, profile images are fungible, so uploading immediately and applying isn’t earth-shatteringly important, especially since I can just swap one out if I decide I don’t like it. Other things, like changing a password or setting website or Discord info, can all be posted at once even if there was no change made.