Skip to content

Latest commit

 

History

History
146 lines (80 loc) · 17.6 KB

devlog.adoc

File metadata and controls

146 lines (80 loc) · 17.6 KB

CHEER App Devlog

Why React Native?

I chose to use React Native for two simple reasons: it’s cross-platform and I’m already familiar with the React workflow. I’m open to the idea that there are better alternatives, but I feel like React Native is a safe place to start.

Getting Started - 2024-06-20

The React Native docs recommend using the Expo framework. I got it set up and running with the Bun runtime. I found that I had to use expo start with the --tunnel flag to get hot-reloading working. This appears to be a fairly common necessity stemming from any number of local or network-wide configurations. The Expo docs claim it slows down hot-reloading, but it’s been pretty speedy for me. Overall, I’m not too concerned by --tunnel. I did have some trouble with an ngrok dependency for the --tunnel feature, but I fixed it by re-initializing the repo and installing it in the project specifically with bun install.

A side note

This highlights one of my gripes with the JS ecosystem. I find that it’s really easy to get into messy situations with dependencies, especially when Bun and Node get mixed. The error messages never help. At least starting over pretty consistently fixes things.

A Camera Component

A core function of the app is the ability to take a picture with the phone camera and process the image data. Thus, there needs to be some camera view in the app.

Starting with VisionCamera - 2024-06-20

Initially, I thought something like a camera view would be simple. I thought that many system-interaction components would be provided by the OS (hence "Native"). I was wrong. I had to find a pre-made library for a camera view component and functions for handling camera permissions and image capture. I arrived at VisionCamera, since it seemed like the highest-quality option. Since it "relies on native code" [1], I have to use a development build instead of Expo Go. Initially, I thought this meant rebuilding for any changes, but once I actually started reading the docs (and once I had an actual development build on my phone), I understood how it really works: it’s just hot-reloading but in its own dedicated app. ¯\_(ツ)_/¯

I ended the night with a working camera view. It just shows what the camera currently sees. There’s also a dedicated page for requesting camera permissions. This all works on the development build.

Taking a Photo - 2024-06-21

I followed the VisionCamera docs to begin the code to take a photo. I started by trying to make a button that calls takePhoto(), but I was having problems that I thought were related to plugging an async function into a react-native Button component’s onPress prop. I tried plugging in a sync function and got the same error. I read the docs, and it turns out the native button component actually doesn’t take any child components, just a title prop for the text with which to label the button.

The takePhoto function saves the photo to a temporary file and returns an object with some image data and a path property. To demonstrate the basic photo capture functionality, I added a stateful vairable photo and a conditionally rendered Image component that displays the image at the path returned by takePhoto.

OAuth

Another core feature of the app is tracking who is submitting data, on top of simply tracking attendance at harvests. In the interest of not rolling my own auth, I decided to stick with OAuth, starting with Google and ICloud (for ease of use with Android and iOS systems respectively).

Getting Started with Google OAuth - 2024-06-21

I’ve never worked with any sort of authentication system before, so I’ve got a lot of learning to do. I decided to start with Google OAuth because I’m more familiar with their account systems and UI standards (I anticipate having to work with some sort of app management site). I found Google’s own React Native sign-in library, which has setup documentation for Expo. The Expo docs themselves also have setup info for Google signin. I followed this video for the most part. Notably, I got the SHA-1 certificate fingerprint from the EAS keystore, and I’m not making a webapp so I didn’t do any of the web configuration.

A Button that Works - 2024-06-24

I took a couple of days off, but I got back to work with a proper schedule in place. The first thing I did was establish tab navigation in the app, following the docs. Then, on the 'User' page, I continued following the aformentioned tutorial video. At the point where the actual Google APIs begin to be used, I began encountering errors. At this point, I decided to look for a different source. I thought that I might be watching a video that was out-of-date or simply didn’t quite have what I was looking for. This was mainly sparked by the realization that the repo I mentioned earlier wasn’t mentioned in the video. So, I found a different tutorial posted by Supabase that used that repo, rolled back my code to the working camera, and started fresh.

This worked out great. Using the google sign-in button is very straightforward. I prematurely shut down the google cloud project thinking I didn’t need it, but I lost very little progress. Right now, I can get google user info by signing in using google play services on the device.

Switching to Firebase - 2024-06-25

This was a pretty bad day. I slept in, so I got a late start. It was also the release of the Riven remake, so I had to wait for the game to download (21GB) before I could start working (not multitasking on the desktop allowed the install to go quickly). All of this meant that I had half the normal time to get anything done. Nonetheless, I persisted. My goal for the day was to specify a client ID for the Google signin so that a consent screen would appear (still just following a tutorial). The thing is, the video was also a bit dated, so it took some digging to figure out how to configure my app to use the right client ID. I eventually found this page. Turns out the right way to do this stuff on Android is now "Credential Manager", but the only information I could find about working with that is for Android studio, not Expo. I slowed down a bit, and looked at my options. I realized that, through all of this process, I had been jumping past instructions for "with Firebase" that looked much simpler. So, I decided I would try to work with Firebase instead of Google Cloud. This allowed me to avoid going over to credential manager, and still follow along with the same video.

Or so I thought. It took some work [2], but I got the Firebase config into the app config. Even still, I got no OAuth consent screen. Thus, I decided to try and find tutorials for authentication in Firebase, not worrying about Expo.

Successfully Linking the Client ID - 2024-06-26

I found this video that was exactly what I was looking for. Google signin in expo with firebase. There was a bit of hassle, but I finally confirmed that the app was using the right client ID by having the sign-in button use a Google Drive read-only scope, and seeing that the OAuth consent screen had the correct app name.

Successful Firebase sign-in - 2024-06-27

I started by removing the google sign-in plugin from the app, and following Firebase’s web docs by using the signInWithPopup function. That didn’t work, and I soon found out that was because that is a web-only function that creates a new browser pop-up window. Instead, the correct way to do it in react native is to use the google sign-in button, then use the GoogleAuthProvider.credential function to create an AuthCredential based on the tokens provided by the GoogleSignin object from the react native google sign-in library. We are now successfully authenticating users and registering new ones using Google OAuth. B)

Caching Credentials - 2024-07-03

This was a simple and smooth addition. Using Expo’s SecureStorage config plugin, we can store plaintext key-value pairs securely on the device [3]. I store the idToken, accessToken, and authentication provider, and generate a Firestore credential object with that information. Right now, I don’t have any proper handling of expired credentials, and the login button shows while the cached credentials are being loaded. I’m going to move onto other crucial features and take note of these issues here for later cleanup.

App-Wide Auth Session Context - 2024-07-10

Something I didn’t anticipate was that the Firebase app state isn’t persisted across pages with my current import method. I could just have a first-load side-effect that signs in with the cached credentials, but that feels inefficient and unnecessary. Instead, I decided to use React’s useContext hook to create app-wide state. The app-wide layout initializes the context and passes it to the tabs screen, and the tab layout tries to log in with any cached credentials, then passes the context to the given tab. Thus, the Firebase app stays the same between pages, and the log-in only has to happen once.

Locking the Form Behind Login - 2024-07-12

This is a pretty basic requirement. However, it took me a while. I spend the first half of the day under the misconception that putting the context in the layout would actually just give each route its own copy of the context. I followed a confusing tutorial and eventually realized that it was just a fancier way of doing what I already was doing, and that what I was doing worked fine. Afterwards, I struggled with my inexperience with React, as I spend far too long being confused because I didn’t realize that setState only re-renders if the new state is different from the old one. At the end of the day, I made the home page display a warning with a link to the user page when the user is not logged in. Pretty small, but I learned a lot along the way.

Correctly Persisting Authentication - 2024-07-15

I was having a problem where the persisted authentication would become invalid after only a few hours. I realized I was improperly using the token model. I was storing the access token, which is only meant to be used at initial authentication. The proper way to do it is to store an ID token that is provided by the server. I tried to figure out how to do that manually, but I couldn’t. Eventually, I discovered a function in the Firebase API that mentioned persistence and, after some digging, found that there is a specific set of functions to run to get persistence working in a React Native app automatically. Now, it works like a charm.

Working with a Database

The cloud has to store the following: (may change)

  • Volunteer attendance

  • Harvest data (I’m not sure what specifically this entails)

There may be a number of different approaches to these requirements. We’re pretty set, however, on using Firestore, for its easy integration with our already-existing Firebase project.

Syncing User Information - 2024-07-02

I started by having a collection of people in the database, indexed by their Firebase UID, containing their first and last name. On the user page, when the Firebase user is loaded, I check to see if that UID is in the collection. If it is, I simply get the first and last name and display them. If it isn’t, I create the document and get the first and last name from the specific OAUTH provider, uploading them to the database and displaying them in the app. There were some things I learned in the process of adding this feature:

  • I learned some basic Firestore rule management. Right now, any request to the firestore must be authenticated (so the client must have logged in).

  • I got a decent handle on what kinds of data goes in and comes out of the Firestore API.

Right now, I just use typecasting in the typescript code. However, in the future, it will very likely be in my interest to use Firestore rules to enforce a schema on the data. I have heard of some libraries that introduce generics to the firestore JS API, but I haven’t looked too deeply into them yet. I’m not sure if that will be necessary or not.

The Harvest Form

The core of the app is a form that allows users to log harvested produce. We need to store the following information:

  • Date of harvest

  • Person who harvested

  • Garden where the harvest was made

  • How much was harvested

  • Unit of measure

Database Relations and Form UI - 2024-07-22

My dad and I decided to put this form together in small steps, checking in with each other along the way. We decided to use references to other documents to achieve a similar effect to a relational database. The first had a hard-coded list of options in a dropdown, and a submit button. The selection from the dropdown wasn’t uploaded, just the user and the date. Next, my dad put all 6 gardens in a collection in the database, and I wrote code that would use that list for the options of gardens in the form. Then, based on the selection, a new field on the harvest was added that held a reference to the garden that was selected. This all went very smoothly.

Full Form - 2024-07-23

After a meeting, we got a pretty solidified idea of how to structure the database. For all front-facing data (names of crops, names of units, etc.), we decided to have a collection with documents IDed by locale for dynamic fetching by language. This works great. We’re now submitting every field we so far want. Next is attendance and some UI cleanup.

Bits and Pieces + Total Harvested Today - 2024-07-25

I added a piece of text below the measure input box that shows the total amount that crop harvested at that garden on the current day. I also improved the way that I was handling dates: Firebase’s JS API uses Timestamp objects rather than Date objects (I only learned this today), so I had to switch up a few things. I also changed the behavior of how the measure input is stored. Previously, the variable itself was a number. However, this meant that if any invalid string was input, it would parse to NaN, and the sudden change in text crashed the app (at least, I think that’s what caused the crash). I fixed this by storing the variable as a string and only parsing it when it got sent to the cloud. This meant that I could pass it directly into the value property of the TextInput, and also that I could allow the invalid string '.' temporarily (while hiding the submit button) so that users could type values such as .4. I also did some better regex that prevents the user from typing an invalid float.

Improved UX Flow - 2024-07-26

I changed the behavior of the app upon launching. I added a welcome screen that prompts the user to either begin harvesting or just log attendance. I also made a number of backend improvements, as well as added the status bar into the layout (so now items are properly centered).

Attendance

Gardeners who attend frequently are rewarded with credits they can use at certain farmers' markets. Thus, it’s important to track attendance separately from harvests.

Attendance tracking - 2024-07-24

I decided to add a new tab to the app that’s dedicated to logging attendance on the current day and viewing past attendance. I used this calendar component with some custom styling to fit the rest of the app and to make the information more clear. I also automatically logged attendance when the user submits a harvest, using this toast component to alert the user of that action.

Miscellaneous App Improvements

Localization - 2024-07-11

A number of the CHEER’s volunteers primarily speak Spanish, so proper localization is necessary. I tried using lingui, but got some strange errors with imported objects beinv undefined. I couldn’t find anyone else with the same issue, so I decided to go to something else. I ended up using i18n-js because that’s what’s used in the Expo tutorial. I also created a custom hook and added a piece of context so the app would re-render when the system language changes.

Expo for a newb dinosaur - 2024-09-28 - David Droddy

So, I want to build things myself without a service. Looks like I need Android Studio and a JDK for that. Which helped me realize that, I will have no choice but to use the Expo build service for the iOS version unless I get my hands on a mac I can use to build the iOS version.

aaaaand, the Android dev environment setup didn’t go well. Problems with Watchman install mainly but then a problem with the android emulator as well.


1. See this issue
2. Expo recommends uploading the Firebase config file to EAS as a secret. I did that, it was easy. But, it turns out that you have to use an `app.config.js` file to use secrets as part of the app config. I saw in the docs that you could use `app.json` then just add to it in the js file, but I couldn’t quite get it to work. I just ended up copying the entire `app.json` file into the `app.config.js` file and it worked.
3. I’m not sure of the specifics of this library (what makes it "secure", how it works under the hood, etc.), but right now I don’t need to. It may come up later, however.