The other day my dad went to buy some house supplies. Just as he was about to checkout, another customer offered him a gift card in exchange for cash at a discount (the gift card was given as store credit for a previous item he returned). After some thought my dad agreed and, after verifying the balance, paid him the cash. My dad then purchased the supplies with the card, afterwards which still had a substantial balance.
He explained what happened after he picked me up and asked if I saw any problems. At first I saw no issue with this: he got cashback from the store, so the customer could not have gotten any personal information. It might have been possible to lookup (future) purchases, but so could a casual observer.
But then the cynic in me came out: Was it possible to purchase something online using a gift card? I checked the website FAQ and sure enough, it was possible.
However I wasn’t too concerned. If this was a scam all along then the money would already be long gone. Furthermore the way that my dad told the story made the other customer sound like he just wanted the liquidity of cash and didn’t have any malicious intent.
We drove back to the store and checked the balance of the card. It was the correct amount. Since we were already at the store and didn’t want to take any chances, we bought some more items to use up the balance (we couldn’t exchange for a new gift card).
As uneventful (albeit random) this story is, I think there were a few interesting learnings here in no particular order.
I was aware of the market where people sell gift cards at a discount online but this was the first time I’ve witnessed it in person. I’ve never really trusted online markets because, if anything went wrong, it can be very frustrating to find someone who can be responsible for the loss. However this risk is significantly reduced if the transaction is in person, and by personally verifying the card balance.
I thought it was unfortunate that the other customer had to sell the gift card at a discount. However there was no way of knowing his reasoning and that was no business of mine. Ultimately it was his decision to offer the balance at the discounted rate. What mattered was that both parties were content with the deal.
It was also interesting to think of this transaction from the view of an investor. Suppose the offer was $200 for a $250 gift card. The potential return would be 25%. Of course there is also the potential of losing 100%, but everything has a risk. What would you have done?
From a developer’s perspective, it was kind of fun to try to think outside the box. For me when dealing with these kinds of questions, I find it easier to just assume there was a solution/scam, which would allow me to focus on how it could have been done.
I try to start with the assumption of good intentions when dealing with strangers. I think this type of behaviour is increasingly important in the society that we have today. In a way, this is the continuous version of prisoner’s dilemma. However, as the saying goes, it is equally important to “trust but verify”.
Thank you to whoever sold that gift card.
One of the key metrics for advertising companies is conversion rates. This might be the percentage of people who click on an ad or install an app. Targeted ads are commonly used to identify the right audience to display an ad to, thus increasing the likelihood that they get converted.
So how do advertisers know who to target? To do so requires a great deal of knowledge about an individual. This might be gathered by tracking the apps and websites visited, one’s daily routines, and the network of friends on social media. I think many people put off privacy because its benefits are not immediate nor obvious. One (hypothetical) malicious use is if an insurance company discriminates prices based one’s usage pattern.
Nonetheless there are a few valid use cases for limited tracking. For example services want to identify users who try to reinstall an app to get a new free trial or replace their previous fraudulent account. Or perhaps companies want to give users a discount for upgrading to a new version or when buying multiple apps1.
How tracking works
Suppose someone builds a tracking library (Triple Tap seems fitting) and convinces many apps to include it. What are some ways it can amalgamate user sessions across apps? Nowadays everyone uses a NAT router so there might be hundreds or even thousands of users behind a single IP address. The easiest way to distinguish users is to generate a UUID on startup. However, since mobile applications are sandboxed and cannot communicate with other apps, the library must rely on some information from the operating system to ensure consistency between apps. Assuming this library is included in a lot of mobile apps, it is then possible to build a list of apps used by a single user.
[iOS 2 - 6] UDID
Historically iOS provided a convenient method
[[UIDevice currentDevice] uniqueIdentifier].
A unique device identifier is a hash value composed from various hardware identifiers such as the device’s serial number. It is guaranteed to be unique for every device but cannot publicly be tied to a user account. […]
Unfortunately there are several downsides of this identifier from a privacy standpoint. Although this cannot publicly be tied to a user account, many apps do require users to sign in, thus allowing them to establish a link. Furthermore, since this is based on the physical device identifier, the same identifier will be generated even after you sell or recycle your device. Luckily the general community has become a lot more privacy conscious and this was deprecated in iOS 5 and removed by iOS 7.
[iOS 6 - Present] IDFA
In iOS 6 Apple introduced Identifier for Advertisers (IDFA) to replace the deprecated UDID. This identifier was created specifically for advertising and tracking and provided some benefits for privacy such as the ability for users to generate a new identifier. It also provided an option for users to request limited ad-tracking which, similar to the Do Not Track HTTP header, only informs that user do not want to be tracked. It isn’t until iOS 10 that this identifier will return nil when the option is enabled.
[iOS 6 - Present] identifierForVendor
At the same time, Apple introduced an API specifically allowing apps from a developer to obtain the same user identifier from all installed apps on a device published by that developer. Tracking libraries cannot distinguish users using this method because apps from different developers will return different identifiers.
[iOS 2 - 11] MAC Addresses
When UDID was replaced by IDFA, some people were just not content with an identifier that could be reset. Thus many applications began using the wifi MAC address instead. This was short-lived as iOS 7 began returning a constant
02:00:00:00:00:00. Nonetheless it was still possible to use the ARP table to retrieve the MAC address from the wifi router, at least until this was removed iOS 11! Such a classic game of cat and mouse.
[iOS 3 - 8] canOpenURL
Although this API was available since the early iPhone OS era, it seems like only in the last couple of years have apps adopted custom URLs as a priority feature. This makes sense because, with custom URL schemes and the ability to query for its existence, apps can be smarter by properly opening that app, or bringing up the App Store sheet. But who would have thought that any company would have the audacity to scan all the apps on one’s phone? This was addressed by limiting the number of queries and by requiring apps to specify URL schemes they intend to query ahead of time so it can get reviewed2.
[iOS 8 - Present] iCloud Keychain
iCloud Keychain was introduced as users began using multiple devices and as Apple improved its cloud infrastructure. In the simplest form, one can think of it as a key-value dictionary that is synchronized with Apple’s servers and the rest of the user’s devices. However the side effect of this persistence is that data stored on servers will not be deleted, even if the user has deleted the app from all of their devices. This is not to say that Apple is not aware of this issue - it’s just that privacy leakage and the goal of persistence for apps are inherently irreconcilable. In fact iOS 10 betas initially changed the behaviour to perform the deletion, but was reverted due to compatibility problems3. The bright side is that this only allows persistently storing data, but apps still need to find a way of communicating with other apps.
[iOS 11 - Present] DeviceCheck
This provides developers with the ability to verify the authenticity of an Apple device and to store two bits of information on a per-device, per-developer basis. This is now the preferred way to record characteristics about a user such as flagging fraudulent users and free trial usage in a privacy-preserving manner. A lot of thought has been put into this. For example although it provides a queryable timestamp (of when bits are updated), this is trimmed to provide only year and month granularity. This gives 828 (69 years * 12 months) possibilities if we assume date ranges can be between January 1970 - January 2030. However this can easily be (if not already) constrained to only accept timestamps within the last few months.
User tracking is common practice today, mostly for ad targeting. To do so ad tracking libraries must be able to distinguish users behind NAT routers, which may be in the hundreds or thousands. Furthermore apps must rely on the operating system in order to acquire a consistent identifier between apps. Historically iOS provided several convenient APIs to do so, however these identifiers could identify users and persisted between installs.
iOS removed many sources of persistent identifiers and replaced them with privacy-respecting alternatives that provide most, if not all, of the necessary functionality. Nonetheless there are still other sources, albeit small (eg. carrier name, device name, model, version), which together can be used to fingerprint a device. Maybe we should all go back to using a Blackberry with separate work/personal profiles?
Note: All the iOS versions affected signified by the square brackets are inclusive.
3 ↩ Ironic anecdote: Deleting iCloud Keychain probably would have prevented crashes on Frenzy if users had just restored their phone. This was due to the app trying to failing to decrypt the database since it was using the key from the previous phone (backed up) on a newly created database (not backed up).
A selection of other sneaky methods:
One night there was a spider in the shower. I wasn’t going to use it so I just let it be. The following morning I thought it was gone. But then I noticed it was hiding in a little groove along the ceramic tiles. The surprising thing was that by the time I stepped into the shower the spider had already scaled up the wall towards the door.
The water from the shower head soon splashed everywhere, including the ledge that the spider was on.
There must be something that triggered the spider to move away from its spot - whether the time of day, sound of curtains (can they hear?), or maybe just pure boredom - but we can only make an educated guess, just like we can never truly understand the reason behind decisions made by a neural net. Guess these creatures have more in common than just nets—
Just today I realized how much better OLED screens are vs. LCD ones in the sun. I could easily read messages on my watch compared to my phone (both were on max brightness).
I have high hopes for the (presumed) lack of Touch ID because I can see how this added level of magic will be a (smaller) jump in convenience. It’s sort of like how Apple Watch unlocking unnecessarily promotes laziness (both taking breaks and typing)!
The dual camera system takes stunning pictures and would be amazing if it becomes available in the regular-sized phone. However this would be challenging to fit in a device where the screen takes up a much larger proportion of space.
On a similar note, Steve Jobs theatre also has no bezels, which is pretty fitting. Looking forward!
I am really excited that Frenzy 3.0 was launched this week. This major release introduced a new UI to make sales and Drops more discoverable. Amongst all the new features and polish, I am really proud of the custom refresh animation that I worked on. Here I explain the journey from prototype to completion.
CAShapeLayer is amazing because it provides any easy way to animate stroking a path by adjusting the
strokeEnd properties. My first thought was to Google “uifont to cgpath” and, of course, the first link was to an SO post with a simple function to do just that.
However there were two main problems:
- The Frenzy logo is based on a font but has modifications to combine the “f” and “r” done in Illustrator, so we couldn’t just use the font directly.
- And even if we could, the path generated by this function represents the outline of the letter. This means that when the renderer tries to animate filling the path, it has no idea which pixel should or shouldn’t be filled for the current keyframe, which results in all-or-nothing fills.
I knew the best result would be if the letters were stroked. Nevertheless, I began making a prototype using the SO code to see far I could get with as little custom code as possible.
I created a playgrounds file which animated the drawing of a single letter. Having a grasp of the concept, I then created separate layers for each letter. Since the canonical version of the logo that we had was in Illustrator, I generated the bezier paths using PaintCode, which was a timesaver. This is how it looked:
Although a bit better than the default spinner, this wasn’t in line with the frenzy brand - the use of an outline and white fill looked out of place
The real problem was that we needed some way of representing the stroke path of the letters. Although we probably could have created a replica of the logo by combining (many!) bezier curves with varying line widths and animating it, this would be a lot of effort and detail for an animation that will be displayed fairly small in size.
Instead we can get a very good approximation by stroking the rough path of the letters and clipping to the actually outline of the logo. At the size that the custom refresh was being displayed, this was good enough.
CAShapeLayer makes it really easy to tie the tableview scroll offset with the stroke-completion amount. Although not obvious, the timing for the erase-then-stroke animation was heavily tweaked so that it paused long enough to emphasize the logo, but quick enough so it wouldn’t become cliché. Lastly, the logo will always complete the full animation loop before being hidden, even if data came back midway in the animation cycle. This gives the impression that each animation is deliberate and refined.
I have always loved working on the subtle polish features that create an ah-ha moment for users. I am very proud for this to be my finale feature in both senses of the word. The past year has been filled with great memories and experiences with the whole team. I enjoyed every moment and learned a lot in every aspect. Although I will miss the team, I know they will continue to iterate on the product and gain new collaborations. I look forward to seeing what’s yet to Drop.