Couple of months ago I decided to write an app that I can use to keep track of my expenses. I wanted a contemporary-looking app, with no ads, for free. I wanted to give mobile app development a try. In January I did an AWS training, so I also wanted to apply what I’ve learned, for a backend API.
In this post I will share some parts of the process of designing, implementing and deploying the Para app, together with some of the difficulties and bugs I’ve encountered along the way.
The aim is to share my findings and ultimately help people that are new to mobile app development, and in particular to the NativeScript platform.
I will focus on the app itself. I’ll talk about the backend API in another post. The API is developed with Python + Flask & DynamoDB, deployed on AWS via Zappa.
The code for the mobile app (TypeScript) and the backend (Python) is open-sourced.
The article starts with discussion about Firebase Auth and how I’ve used it. If you only care about the NativeScript-specific gotchas — scroll down to the Helpful NativeScript-specific plugins and gotchas section.
Introduction
When planning how to make the app, I spent some time researching platforms for cross-platform mobile app development. I wanted to write the code once, and then be able to run Para natively on both Android & iOS. The main candidate platforms were React Native and NativeScript. I already had some experience with JavaScript/TypeScript, but none with React*. NativeScript lets you use pure TypeScript and it’s developed by Telerik, which was originally a Bulgarian company. Also, it seemed that the majority of the comparison articles I found on Google, were suggesting that NativeScript is better.
All of this was enough for me to try NativeScript first.
Reading this post will definitely be more effective if you’ve already read the official NativeScript Getting Started guide.
* I know, I know, it’s next on the list.
Definition of done
- App to help me keep track of my expenses.
- Easy to add new expenses and view existing ones.
- Supports email & social login.
- Offers statistics (“How much I’ve spent this week/month”).
- Costs me 0$ per month to run the app. Given that it’s published on an app store and has more users than me and my dad.
- Runs natively on Android and iOS.
- Can handle 500 users that simultaneously use the app.
- Can handle a user which uses the app on multiple devices.
10 000 ft. overview
- validating & collecting user input (when adding new expenses / updating an expense)
- displaying the user’s expenses + statistics
The API is responsible for:
- validate & store expenses to a database
- retrieve expenses and stats about them from the database
Firebase is responsible for:
*storing user sensitive auth data
- auth related activities like email confirmation, resetting passwords, etc.
Both the app and the API share the same representation of what is an expense (declaratively, by using a json schema).
By design, no user credentials (emails, passwords) are stored in the database I manage (the DynamoDB).
Authentication and Firebase
The difficulty — how to authenticate the mobile app to the backend API with minimal effort, while offering an adequate solution.
I used Firebase for auth related activities in the app. Firebase identifies each user of a given project with an user uid
(it’s a long string).
When a user logs in (either by entering email + password or via Facebook), his user uid
is accessible from within the app code. Currently, in the context of the app, all expenses belong to the currently logged in user, thus the uid doesn’t appear in the schema of expense objects.
The uid is useful when the app communicates with the backend API though — because the API needs to perform CRUD operations in the context of the correct user.
Sending a uid directly would not be safe because impersonating users would be possible. Firebase offers the ability to generate ID Tokens. Here’s my mental model about them. It’s a JSON Web Token (JWT), a string, generated by the Firebase Client SDK (i.e. a SDK that runs on the phone). The string contains the currently logged-in user’s uid, the time the user logged in and some other user info (email, etc.), which Firebase has about the user. The nice thing about this string is that when our backend receives it, the server can verify the integrity of the string — i.e. if the string says that the user with uid XXXYYY sent the string — we can be certain that indeed it was this user that sent the string.
The general workflow is that when the phone makes an API request to our backend, the ID Token is included in a HTTP header. When the server receives the request, it decodes the token, by using the Firebase Admin SDK. Then the user uid
is available to the server and it’s possible to process the request in the correct context.
Why I chose this approach? It releases me from the hassle of worrying about storing user emails, passwords, dealing with email confirmation, forgotten passwords, etc. It is a hassle. And since it’s easy to mess it up, I delegated that to Firebase. All I care about is ensuring I can show the login/signup page to my app users and once they’ve enter what they should, ask Firebase to give me info about the user (his uid and id token).
Another noteworthy aspect of using Firebase is having isolated environments.
I want to have completely separate development and production environments. Meaning, that I can develop locally and not touch the production Firebase project — which has all my users. Achieving this is a matter of creating a per-environment Firebase project and choosing the correct Firebase configuration file during app build (or “prepare”) time. I have the production file stored securely and can use it to make production app builds which are uploaded to the app/play store. I can share the development configuration with other collaborators so that they can develop locally.
Since the backend’s Firebase Admin SDK needs to have credentials to the same Firebase project, I store the credentials as an environmental variable — and the correct credentials are used for a given server environment. This is really handy because it’s not possible for a development app to access a production server — can happen if I misconfigure the app (e.g. point to the prod API when I develop locally). You can tell I don’t trust myself a lot :)
That’s it for the 10,000 ft. overview.
I will show some of the NativeScript-specific plugins I found useful and some problems / gotchas related to them.
Helpful NativeScript-specific plugins and gotchas
When using NativeScript, you can benefit from npm packages. E.g. I used the popular moment.js
and underscore
JavaScript libraries without any issues. Here’s a page with verified NativeScript-targeted packages. Here’s a page with overview of how to install and use them.
Just as a quick note, be careful when installing packages from npm — sometimes the packages assume they’ll run either in a browser or a Node.js environment — and can fail if ran within NativeScript (e.g. I saw a few packages failing to import process
).
nativescript-plugin-firebase
Instead of using directly the official Firebase Android/iOS Client SDKs, I used the nativescript-plugin-firebase. The benefit of that is that I don’t need to write Java and Swift — I write TypeScript, once. As mentioned above, I use Firebase for all things that are auth — registering, signing in and logging out users. Changing their password and recovering their password are also made possible by the plugin. And most importantly, getting the user’s uid and ID Token.
The one thing that wasn’t immediately obvious to me was how to ensure that once the user logs in, he stays logged in, until he explicitly logs out. The problem was that if you login and then minimize the app and restore it after an hour, auto logging in will succeed, however the ID Token will be expired — tokens are only valid for an hour since their creation (i.e. last time you entered email + password and signed in). And if I try to send an expired token to the backend — the backend will bark.
The trick, that worked for me, is to use firebase.getAuthToken({forceRefresh: true})
when auto-signing in the user. This will force the creation of a fresh ID Token. Here’s an example usage. In the code I check if the user has already logged in, and if so, request a fresh token immediately. The login-page is the default first page of the app and it’s shown always when you open the app. The check from above ensures that if a user is logged in, he’ll be redirected to the “home” page directly.
nativescript-pro-ui
The pro-ui package (it’s actually a bundle of packages since a couple of weeks) provides reusable UI components.
I use it for three different things in my app — for the list of all expenses, for the side drawer and for a data-form to create/edit expenses.
RadListView
Why using the Pro UI’s RadListView is indeed rad? Because all I need to do to show the list of expenses from above is: say how each expense of the list should be displayed (i.e. given an expense data object, which attributes to show and where to put them). That’s done via XML, provide a collection of expenses to show (it’s called an ObservableArray and you can think of it as a beefed-up array that emits events when you add/delete to it)
The nice thing is that there’s a binding between the collection of expenses I provide and the RadListView — i.e. if I add/remove an expense to my collection, the RadListView will automatically update. This means I can only focus on making sure my array has the correct items and not on UI.
Apart from that, I get “free” pull-to-refresh and loading data in batches (i.e. 10 expenses per batch are loaded). To get the former two I only need to provide functions which should actually refresh the data / get the next batch of items from the API and update the items in the ObservableArray.
There’s an important gotcha here. When writing the XML markup for the “list all expenses” page, I wanted to show different things depending on whether the user has any expenses or not. If the user doesn’t have expenses — I show a message together with an “add new” button, if there are expenses — just show them. I implemented this using styles (think CSS) by
RadListView visibility=”{{hasItems ? ‘visible’ : ‘collapse’}}”
. So I use some boolean variable hasItems
which determines whether we see the list or not.
The trick is that one might think that since the collection of expenses we provide is of type ObservableArray, the .length
property is also observed. Well, it seems it’s not. So binding to the expression expenses.length !== 0
is not possible. My workaround was to subscribe to the events of the ObservableArray and adjust the value of hasExpenses
on each add/remove.
If the above doesn’t make a lot of sense, make sure you’ve read the section about binding in the documentation.
RadSideDrawer
The drawer is quite handy — you can put the hamburger on the top left of each screen and when you press it or when you slide from left to right, the side panel drawer appears.
It’s easy to have a lot of duplicated code this way though — i.e. on each page you want a side drawer, you write essentially the same code — the content of the drawer and then the content of the page.
I extracted the code for the drawer content into a separate component. I found this article really informative about how to extract NativeScript UI components and reduce duplication.
Frankly, I now have the equivalent of “import the side drawer” code in most of my pages which is sort of duplication as well. As a further refactoring I plan to extend the Page
class to PageWithDrawer
and use it as the root element of my pages.
RadDataForm
Oh, this one’s my favourite. It’s really handy. Essentially, you provide a plain old data object, and optionally description of the properties of the object, and you get a UI data form. You can register callbacks called during validation of user entered data. You choose when the data the user entered is committed to the input data object. You can also register a callback for when data is committed. You choose if committing input to the data object happens immediately or after the user has, say, pressed the Submit button.
You get different field types out of the box (e.g. text, number, email, etc.) — handy because you get some auto-validation and the proper keyboard is shown.
The easiest way to configure the visible fields, their type (text, number) and validators (MinLength, NonEmpty, etc.) is via XML markup.
<RadDataForm.properties>
<EntityProperty name="name" displayName="Name" index="0" />
<EntityProperty name="age" displayName="Age" index="1" >
<EntityProperty.editor>
<PropertyEditor type="Number" />
<EntityProperty.validators>
<RangeValidator minimum="1" maximum="150" />
</EntityProperty.validators>
</EntityProperty.editor>
</EntityProperty>
</RadDataForm.properties>
I recommend reading all of the docs on RadDataForm if you plan to use it. It’s not long and it will save you a ton of time.
During development of the app, I found myself in a situation in which I needed very similar dataforms with the only difference being the action performed after the Submit is pressed. The prime example — creating a new expense or updating an existing one. From the perspective of the DataForm, in both cases the input data object has the same shape, with the difference that during updating an expense, the data object has actual data. When the user presses the Submit button of the form, a different API endpoint is called. But for the rest, the form is the same — validation, field types, etc.
Thankfully, when creating data forms, instead of using just XML markup to describe the properties, you can pass JSON. So instead of just passing the source data object and using XML, you pass the data object and a “metadata” JSON, which describes the properties instead. Here’s how the above XML can look as JSON metadata:
{
"name":"name",
"displayName":"Name",
"index":0
},
{
"name":"age",
"displayName":"Age",
"index":1,
"editor":"Number",
"validators":[
{
"name":"RangeValidator",
"params":{
"minimum":1,
"maximum":150
}
}
]
}
The examples from the NativeScript’s ui-samples-repo, seem to all read the metadata from a JSON and pass it directly to the DataForm. What I did was to have a function which generates the JSON. This gave me a lot of flexibility when generating the metadata. There’s info in the docs on what you can and cannot do with JSON compared to via XML markup.
And now the gotcha! :) I spent good couple of days chasing a bug related to RadDataForm. Essentially I was validating the form manually and committing the input to the data object manually too. So after doing that I was accessing the data object’s properties because I needed their values. The bug was that if the property was marked as a Number or Decimal in the metadata of the form, getting its value doesn’t return the number but rather some object of unknown type. The workaround is to call the toString()
of this object to get the string representation of the number and then convert it to a proper number. Nasty.
i18n
I only found out last week that it means internationalization :)
I wanted to make my app available in Bulgarian and English. I googled around for a NativeScript i18n package that is actively developed and came across — nativescript-localize.
The only gotcha here. When doing debug builds it’s ok to do L(“natural language in english”)
and only define a translation in the non-english langs for the string — it will fallback to the english string automatically if needed. However, when doing a release build, the lint will bark. The article mentions some fixes, but as far as I can tell they are not applicable when using the nativescript-localize package because the package generates the string.xml on build time. In essence you might want to use non-user-friendly strings and provide natural language translations for them for each supported language.
I recently found out that the popular i18next JavaScript package also seems to work — probs because it makes very conservative assumptions about the runtime.
Using the library within TypeScript/JavaScript doesn’t deviate from the package’s suggested way. However, in NativeScript we want to translate string in XML pages too. The gotcha is setting the app resources (i.e. making a function available within XML). Doing it like this seems to work correctly:
app.setResources({
‘L’ : (…args) => i18next.t.apply(i18next, args)
});
This enables us to do stuff like:
<Label text=”{{‘translation for key is ’ + L(‘key’)}}”/>
JSON Schema
Apparently It’s not that easy to find a package to do validation of a JSON Schema in a NativeScript app.
After a lot of trial and error, I found a JavaScript package that works well. It’s called tv4. Frankly, I haven’t tried more advanced use-cases like using schemas from different files so I can’t say how it behaves there.
Mocking in tests
I like mocks a lot when testing. I tried to use different mocking frameworks. Most often the problem with the frameworks is that they make the assumption that the tests run in node/browser which is problematic in our case.
I had success only with using the built-in mocking mechanism of Jasmine. It does what I need, but if you know a different package — I’d be super curious to check it out :)
Misc
- SideKick — if you like GUIs it can be helpful. For me the greatest benefit of it are the cloud builds — it lets me build for Android/iOS from my Linux host.
- AWS Device Farm — I test my release builds of the app there. There’s a selection of iOS/Android devices. Not the most up-to-dated though. You get 1000/month within AWS’s free tier.
Conclusion
Overall, I enjoyed my experience developing the app. I enjoyed the fact that I can use npm packages and not reinvent the wheel for each problem. Haven’t tested the app on iOS yet (don’t have a dev account yet) but in theory it should work with no code changes.
I wish I started using hooks
earlier — I use it to choose/change my app config based on whether I am building a development app or a release one. I also use it to make checks like “Is my API address in a correct format and is it an address I’ve whitelisted”, to get the current git sha and put it in the app config so I know the exact code that the app runs, etc.
Thanks for reading this. If you have any suggestions or remarks, please share them — I will be grateful.
Top comments (2)
Thank you. Why didn't you use Firebase as noSql database?
Yes - good question. Actually I used Lambda+DynamoDb because I really wanted to get some hands on experience with them. I don't think DynamoDb was the best choice though - it's great for fast inserts and reading a limited set of the newest items, but not that great for aggregations/statistics (like how much money I've spent in the last three months). It's purely a cost issue - I need to provision a lot of RCUs if I want to do these kind of statistics when I have a lot of users. And RCUs cost money. I like the manged nature of it and that I get replication for free but it all comes at a price :)