Side Project: Promo Codes (Part 1 – Intro and Promo Code Creation)

For my recent sponsorship of AppStories’ 99th episodewith Yoink for Mac, iPad and iPhone, I also offered a couple of promo codes to the lucky first people who got to them.
(Developers can create create promo codes for their app in the Mac- and iOS App Store, to give away to others so they can download their app for free. This way, members of the press can try out your app for free, or you can hold a give-away of your app).

Some users find promo code redemption a little clumsy – and I agree. You have to copy the promo code, navigate to the iOS App Store (or Mac App Store, or iTunes on Mac for iOS apps), paste the code and redeem.

I thought it would be nice to have a “landing page” for each promo code, where the user would be able to click a link and be taken to the correct redemption page with the code already filled in, or at least explain how to do it manually.
It also occurred to me it would be neat to track promo codes – like their redemption status and expiration dates.

Born was the idea for ‘Promo Codes’.

In detail, I wanted the following:

  • Have a Mac-, iPhone- and iPad app, well integrated into their respective OS
  • Be able to create promo codes without having to go to App Store Connect
  • Store those (and manually created) promo codes in a web database so I can access them from anywhere using the apps (basically, sync)
  • Track them
    – when were they created
    – what were they created for (or for whom)
    – when would they expire
    – whether they were redeemed or still unused
    – the popularity of a code (view count, last access date)
  • Have a landing page for users to make redemption easier

Of course, the whole thing would hang on my ability to be able to programmatically request promo codes from App Store Connect, but first, I wanted to check out the alternatives.

The Alternatives

“Alternatives” is saying too much, as there’s not a huge market around this, apparently. It’s more like “Alternative”, because I really found just one good solution:

Alternative 1 – Tokens.app

Tokens is an awesome app that lets you do exactly what I detailed above (apart from an iOS app), and – although I haven’t used it personally – I can only recommend it, as I hear only good things, and it’s being recommend all over the place by developers.
If you’re looking for a trusted, tested way of managing your promo codes – go try it out, they do have a demo!

Alternative 2 – No tracking at all (duh)

The other alternative was to forget about it and keep doing what I have been doing regarding promo codes – nothing.
After all, I’ve been doing just fine not worrying about them at all ever since I became an indie developer.

But what fun would that have been?

My Own Solution

I almost purchased Tokens, but for some reason saw a personal challenge in this, to see if I could pull off creating my own service.
I wasn’t worried about the Mac/iOS side of the project. Rather, I was apprehensive about my server-side “skills” – of which I have none.
I “know” a little PHP, and that just from piecing together code from tutorials and StackOverflowquestions, as well as the occasional, small service script here and there (for example, to handle newsletter sign-up from my apps).
Another big question mark was whether I could manage to programmatically create promo codes using App Store Connect – there wouldn’t have been much of a point in the apps if I wasn’t able to do that.

Having a clear vision of what I wanted to achieve, there would be four parts to this project:

  1. A PHP web service with a database (MySQL) for storing all necessary information:
    I wanted a JSON-based REST API that would allow the following:
    – Storing promo codes in a database (with all necessary metadata like dates, campaign titles and view counts)
    – Deleting promo codes
    – Updating specified promo codes
    – Retrieving all promo codes
    – APNs Push to my apps when codes are added or deleted
  2. A user-facing PHP script that would act as the landing page for the promo code they’re about to redeem:
    – Shows the promo code, the app the promo code is for and whether the code is still available
    – Optionally, if the promo code has already been claimed, offer another one from the same pool of that campaign
    – Allow for easy redemption by just clicking a link
  3. A Mac app that:
    – shows me all promo codes and their metadata
    – can request promo codes for any of my apps via App Store Connect
    – can add manually created promo codes
    – can delete promo codes
    – has deep system integration / services for convenience
    – reminds me about expiring promo codes via notifications
  4. An iPad and iPhone app that:
    – does the same as the Mac app

And with that, I started looking into promo code creation.

Programmatically Requesting Promo Codes

The credit regarding the REST APIs used here goes entirely to fastlane (github).
Those people are doing amazing work and without their open source code, I wouldn’t have been able to do any of this – my code was pieced together from theirs.

Requesting promo codes consists of these 7 steps:

  1. Request a “service key” to communicate with App Store Connect
  2. Log in to App Store Connect using the account’s username and password
  3. Retrieve the account’s session data
  4. Selecting a team
  5. Selecting an app for which to request promo codes and retrieving the promo code info (how many codes left?)
  6. Request a certain amount of promo codes
  7. Load the app’s promo code history to retrieve the newly created codes

1) Request a “service key”

We’re off to an easy start:
Call GET https://olympus.itunes.apple.com/v1/app/config?hostname=itunesconnect.apple.com
and get ‘authServiceKey’ from the JSON this returns – done.

2) Log in to App Store Connect

Custom UI for logging in to App Store Connect

This is where it gets a bit more complicated.
First, call POST ‘https://idmsa.apple.com/appleauth/auth/signin’ with this JSON body:

{
“accountName” : <username, apple id>,
“password” : <password>,
“rememberMe” : true
}

and these headers:

Content-Type: application/json
X-Requested-With: XMLHttpRequest
X-Apple-Widget-Key: <the “service key” you retrieved in step 1>
Accept: application/json, text/javascript
Connection: keep-alive

If you receive an HTTP Status of 200 in the response, you’re free to go to the next step (Retrieve the account’s session data) – the user either doesn’t have two-factor authorization set up, or the cookies are still valid from a previous session.
Chances are, though, that you won’t get 200, especially now that Apple is enforcing two-factor authentication.

So you’re more likely to receive an HTTP Status of 409.
– Remember the response’s “x-apple-id-session-id” and “scnt” header values, you’ll need them for the next few calls.
– Call GET ‘https://idmsa.apple.com/appleauth/auth‘ with these headers:

X-Apple-Id-Session-Id: <the according value you just saved>
X-Apple-Widget-Key: <the “service key” you got in step 1>
Accept: application/json
scnt: <the according value you just saved>

This will give us all the necessary information for proceeding – like the registered phone numbers (obfuscated, like you would see on the website, with ‘••••’ instead of digits).
Depending on the information we receive here, we can also figure out if a code has just been sent to the user’s devices (or to a phone number), or if we need to request one.

The user will have received a code at this point if:
– The returned dictionary contains a key “noTrustedDevices” with a boolean value of NO, or does not contain this key at all or
– The returned dictionary only contains one trusted phone number

Otherwise, the user hasn’t set up any devices to be trusted with their login credentials or the account has more than one phone number which the user has to pick from for verification.
In the latter case, we need to present the user with their obfuscated phone numbers, let them pick one and request a code via SMS for that number.
One phone number is represented in the response like this:

{
id = 1,
numberWithDialCode = “+1 •••• ••••••00”,
obfuscatedNumber = “•••• •••••00”,
pushMode = sms
}

The id is what we need for requesting a code via sms:
We call PUT ‘https://idmsa.apple.com/appleauth/verify/phone‘ with the following headers:

X-Apple-Id-Session-Id : <like before>
scnt : <like before>
X-Apple-Widget-Key : <like before>
Accept : application/json
Content-Type : application/json

and a JSON body like this:

{
phoneNumber : {id : <the id of the phone number the user picked>},
mode : sms
}

which, if everything goes well, will return an empty response and send a code via sms to the user’s chosen phone number.

Now that the user has a code, we can show the UI for entering the confirmation code, which we then again send back to Apple:
Depending on whether the code was sent to a trusted device, or by SMS, the URL to call and the JSON body to send are a bit different.
For a “trusted device code”, the URL is POST ‘https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode‘, with this JSON body:

{
securityCode: {code : <the code the user received and then entered>}
}

For a code received by SMS, the URL is POST ‘https://idmsa.apple.com/appleauth/auth/verify/phone/securitycode‘, and this JSON body:

{
securityCode : {code : <the code the user received and then entered> },
phoneNumber : {id : <the id of the selected phone number>},
mode : sms
}

Call either of those with the usual headers and, if everything goes well, you’ll receive an empty response – otherwise, errors are returned (if the supplied code was wrong, for example).

3) Retrieve the account’s session data

Now that we’re logged in, we can continue with our actual objective – requesting promo codes.
First, we need to know what we’re dealing with – mainly, the teams the user is part of.
We do that by calling GET ‘https://olympus.itunes.apple.com/v1/session‘ with the headers

X-Requested-With : XMLHttpRequest
X-Apple-Widget-Key : <the auth key>
Accept : application/json, text/javascript

which will give us a JSON response and everything we need to know – the current user info (“user”), the currently signed-in team (“provider”) and all available teams (“availableProviders”).
In my implementation, I follow that call with another one to retrieve the currently selected team’s apps right away before updating the UI, but for the sake of this post, I’ll explain that in the following points.

4) Selecting a team

Custom UI for selecting a team and an app of that team

As the user might be part of several teams, we need to be able to change the currently selected team so we can retrieve its apps for promo code creation.
This is done by calling POST ‘https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/v1/session/webSession‘ with the usual headers and this JSON body:

{
contentProviderId : <the selected team id>,
dsId : <the logged in user’s personID>
}

This will update your session’s cookies so that the selected team is the active one.

5) Selecting an app and retrieving its promo code info

Custom UI with promo code info loaded for an app

Let’s retrieve the apps for the currently selected team.
It’s a simple GET call (with the usual headers) to ‘https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/manageyourapps/summary/v2‘ and it’ll return a JSON with all the infos we need about the apps. Depending on the UI implementation, we’re particularly interested in an app’s id (“adamId”), its name (“name”), its bundle ID (“bundleId”), vendor ID / sku (“vendorId”) and the platform it’s running on (“platformString”).
We can now present the apps to the user and let them select one.

With a selected app, we can retrieve its promo code info – front and foremost, if there are any promo codes left to be requested for the current version.
We call GET ‘https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/<adamIdOfTheSelectedApp>/promocodes/versions‘ and receive a JSON where we can
– the version and versionID of the app and
– calculate how many codes are left

6) Request a certain amount of promo codes

Knowing everything we need to know, we can finally request the desired amount of promo codes for the app.
We call POST ‘https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/<adamIdOfTheSelectedApp>/promocodes/versions/‘ with a JSON body like:

[
{
numberOfCodes : <amount of codes to request>,
agreedToContract : true
versionId : <the version id of the app retrieved earlier>
}
]

Careful, it’s an array this time, not a dictionary. Yup – that cost me a couple of hours.

It would be convenient if the response would contain the desired promo codes, but sadly, it doesn’t. All it contains is whether the creation was successful or not.
To actually get to the promo codes we need to

7) Load the app’s promo code history

The promo code history contains all promo codes created previously and just now, so we need to know when we started the request process so we can filter out codes we didn’t just create.
We call GET ‘https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/<adamIdOfTheSelectedApp>/promocodes/history‘ and parse the resulting JSON.
Specifically, we’re interested in the code itself, its creation- and expiration dates, its platform and version.

Source Code

You can find the source code (in form of a sample project) over at GitHub.
Enjoy :)

Advertisements

Developer Tip: Custom Icons for Quick Actions

Notice: I’ve moved the blog over to blog.eternalstorms.at – please update your bookmarks!

When creating a custom Quick Action for macOS Mojave’s Finder, you have the option to supply a custom icon:

Creating a custom Quick Action in Automator for macOS Mojave

Selecting an icon in Automator

But this is what happens when you supply an ordinary image in Dark Mode:

A Quick Action's icon without adjustments

or Light Mode:

Unmodified Quick Action icon

(in Light Mode, it’s obviously better, but still not as subdued as it should be, which is more grey than pitch-black, or whatever color your original icon file might have).

Tip #1 – Getting the icon’s color right

The trick is to have the file end in ‘Template’, like ‘MyWorkflowIconTemplate.tiff’. But that’s not all there is to it. When you add a custom icon through Automator, it automatically gets renamed into ‘workflowCustomImage.png’. So no matter what you name your file, it won’t matter.

To fix this, add a custom icon to your Quick Action in Automator, save, switch to Finder and navigate to /Users/yourname/Library/Services/, where your newly saved Quick Action will end up.
There, right-click your Quick Action, select ‘Show Package Contents’, and navigate into Resources, where your icon file will be. Rename it to ‘workflowCustomImageTemplate.png’.
Now, navigate into Contents and open Info.plist. Look for <key>NSIconName</key> and change ‘workflowCustomImage’ to ‘workflowCustomImageTemplate’. Save.

Now the icon will look like this:

Modified Quick Action icon in Dark Mode

Modified Quick Action icon in Light Mode

That’s much better. But there’s another issue we need to take care of:

Touch Bar with oversized Quick Action icon

The icon’s a bit too large, even though in Finder and the “More…” menu, it looks well-sized.

Tip #2 – Getting the Touch Bar icon to play nice

I thought I’d be smart about it, since the “append ‘Template’ “ trick worked, I’d have three differently sized images and append @2x and @3x. But it didn’t work.
The trick is to have a TIFF file with 2 (or 3, to be on the safe side) representations – one at 1x, one at 2x and one at 3x.
And voila, it worked:

Quick Action icon in Touch Bar with properly sized icon

To create the TIFF, I wrote a small app that takes my icon file, creates three NSBitmapImageRep objects from it and adds them to an NSImage instance. Then it writes the NSImage object’s TIFFRepresentation to disk.
Open that file in Preview.app and you’ll notice all three icons inside that one TIFF (you might have to select View – Thumbnails in the menu to show them)
Select the first and select Tools -> Adjust Size… in the menu bar.
Resize the image to 16×16 (which I found to be working nicely) and set its resolution to 72, if it isn’t already.
Select the next thumbnail and resize it to 32×32 at 144 DPI, and the third thumbnail to 48×48 at 216 DPI. Save.
Now repeat Tip #1 with this new file and you’re all set.

I hope this will save you some time, as it cost me plenty ;)

_____
Matthias Gansrigler
 | Founder & Developer – Eternal Storms Software
e: blog@eternalstorms.at
twitter | facebook | instagram

Developer Tip: Today Widgets and macOS Mojave’s Dark Mode

When testing your Today Widget on macOS Mojave and you see something like this using Dark Mode:

Widget with wrong appearance in macOS Mojave's Dark Mode

don’t fret, there’s a rather easy fix.

In your Today Widget’s Info.plist, in NSExtension > NSExtensionAttributes, there’s NSExtensionPointVersion.
For the widget above – with the wrong appearance – the version was set to 2.0:

NSExtensionPointVersion in Info.plist

To see where the problem might be, I created a new widget under macOS Mojave, and it turns out, it uses version 3.0, which also fixes the appearance issue:

New NSExtensionPointVersion in Info.plist

and voilà, it worked like a charm:

Widget with correct appearance in Dark Mode on macOS Mojave

Compatibility

I’ve tested back to macOS El Capitan 10.11 and it worked, it will possibly work for earlier versions of macOS, too.

 

 

Hope it helps :)