Featured

Welcome to FMX (Firemonkey) Blog

I’ve been developing with Delphi for 20 years since Delphi 1 and it’s been an interesting ride, with lots learned each year. I took a break during the “dark years” of Delphi 8 through XE but since then have been using every version of Delphi.

Delphi may not be as popular a language as it used to be, mainly due to fashion changes, the trendy new developers moving away from all-in-one IDEs, and preferring Open Source, web-originating frameworks and languages. However, it’s still a powerful, modern language capable of incredible things, and well worth looking into if you’re new to the coding arena and want to try something that is genuinely cross-platform.

The Firemonkey framework has been the most interesting additions to the Delphi family, adding support for iOS, Android and MacOS, both 32bit and 64bit variants – and now even Linux Desktop and server support. It’s a bold move on Embarcadero’s part but one that will hopefully pay off for them.

Our mainstream product, MyShiftPlanner, is one of the most popular shift worker time management app on the stores and is written completely in Delphi using Firemonkey. It’s taken 5 years to get it where it is and was initially written in XE5 but now runs like a dream under Delphi Rio 10.3.3.

I’ve learned a lot along the way which I wanted to share with anyone else who wants to take advantage of the power of this truly cross-platform framework.

Each article covers a specific topic but mostly helping to extend what Firemonkey does out-of-the-box so you can take advantage of even more for your apps. The blogs will cover how to add in-app subscriptions to your apps, how to include third party frameworks, use more native controls than are supplied by Firemonkey and more. I’ll also include links to other people’s blogs which offer similar benefits so you can find what you need in one place.

I hope you enjoy this blog and find it useful. Please continue to share/ promote the benefits of Delphi and support your fellow Delphi community developers.

Happy coding!

Securing WordPress

WordPress is used by 43% of websites worldwide, so it’s an obvious target for hackers looking to compromise a site. Unfortunately, for lots of reasons mentioned in this article, admins aren’t making this difficult enough for hackers so it’s common to hear of WordPress sites being powned, filled with malware or customer details stolen from a website.

The good news is that it’s easy to protect your WordPress site with a few simple steps, and everything I’m suggesting is free to implement so I hope you find it useful in your fight again the hackers!

I’m writing this to help those who may be concerned about WordPress security. I aim to provide some insight, tools and options to improve your site security. I recently decided to try and improve our site security from DDoS and hack attempts as it was starting to affect the availability of one of our product sites (thankfully we haven’t been hacked) and I knew there must be good ways to better protect it.
This article covers what I discovered, some background from my research, and suggestions based on what we implemented that seem to have helped.

To start on a positive note, here’s the impact these changes made to our site:

Going from 200 attackers to 0 per day by protecting our site

Note: the details below are by no way exhaustive, as new attacks and techniques are invented every day. What is proposed here will give you a great fighting chance of avoiding being attacked but there’s still a small risk so you need to keep monitoring your site regularly for signs of attack and deal with any attempts as quickly as possible.

Attack Vectors

Before getting into what you can do to protect your site, it’s useful to understand what the 2 biggest risks are (and why), so you can see how hackers are compromising vulnerable sites.

1. Passwords

There are infinite combinations of letters, numbers and symbols that can be used to create a password, but somehow a hacker can still seem to “guess” those that are compromised. How do they do this?

First of all, they use an “most frequently used” attack. This means that they basically have a long list of the passwords that people often use, and try each one (automated with a bot often). Ever wondered why you shouldn’t use Password123? That’s because it’s item number 1 on this list!

Often they hacker will also try variants of these, so you may have used Password27 thinking that’s ok… nope. The hacker will try adding different incremental numbers to each password in the list because people tend to just add incremental numbers to their password each time they need to change it. Password17 becomes Password18 so it’s easy to remember.

Once the hacker has exhausted this list, they can perform a Dictionary Attack. This mean they try common words from the dictionary with variations of capital letters and numbers, as this is a common way to create passwords.
Did you think G4r4g3 was more secure than Garage123 as a password? Nope. As most people now swap certain letters for numbers, hackers have become wise to this and will try those combinations. G4r4g3! isn’t any better as they will also try adding ! to the end as this is a common password technique.

There’s also the personal and username attacks that are used. For example, to help people remember their passwords, it isn’t uncommon to include part of the username in the password. A user called Robert may try to use a password of Rob3rt22 as he’ll remember that. That’s something the hacker will try.
In the same vein, a hacker will also try to find valid user accounts by trying the name of those people they know work at the organisation, so having a user called Robert Smith with a username or “rsmith”, “Robert.smith” or similar will make it easy for a hacker to find a valid user account in the first place that they can then try to discover the password for using the above techniques.

If all else fails, and the hacker hasn’t compromised the account using the above techniques, they use good-old Brute Force, where they just try every combination of letters, words, numbers etc until they’re in. Given how fast modern computers are, this can be a successful way of getting in unless you protect yourselves from this (which is also very easy).

Time it takes a hacker to compromise your password

2. Vulnerabilities in Software

No developer wants to introduce bugs in their code or things that can be exploited but unfortunately it’s a constant 2-way battle between increasingly complex software which becomes exponentially harder to test, and hackers who really pride themselves on find a flaw in complicated systems. This is why companies with deep pockets like Apple, Microsoft and Google pay bounties for any security flaw a hacker discovers in their systems – they actually pay hackers to hack their systems to prove how unhackable they are!

But in the real world, developers can only do their best and fix issues they’re aware of so often a new vulnerability is discovered and a new patch or update is delivered by the developers to patch it. These are called zero day vulnerabilities if they’ve only just been discovered.

So in short, the worst thing you can do is forget about your site once it’s been created. You need to make sure you keep maintaining it with the latest patches, updates and changes to keep it secure. The most common WordPress compromises you hear of will be on sites that rarely become maintained. Sites where the author wrote their articles then haven’t touched it for a while.

Securing Your Site

There may only be 2 vectors mentioned above but each includes a wide range of ways an attacker can use those to get into your site, so you will need to employ a number of changes to secure yourself properly.

1. Update your PHP, WordPress and Plugin versions

WordPress is written using the PHP framework. PHP has been around forever (or feels like it) and wasn’t originally designed with hackers and security in mind, so contained a lot of easy ways to attack sites built with it. It has improved but it’s always best to use the latest version you can. At time of writing the most widely supported version is v8.1 so make sure you upgrade to that ASAP.

WordPress itself is constantly being updated with new security patches and features to make sure you’re using the latest version of that too (v6.2 at time of writing).

Plugins are your only challenge here. If you use a lot of plugins, you will need to make sure they’re all updated to work with the latest WordPress version or they can cause your site to break. This is one of the biggest headaches with WordPress, but checking with the plugin author via their website should tell you if it has been updated or not.

In general, try to avoid plugins that haven’t been updated in the past 1 month as they may be abandoned (as a lot are) so won’t be patched up for new WordPress versions or to fix security issues.

If a plugin hasn’t had an update for 2 months replace it with one that has.

2. User accounts and Passwords

The password attack section was large because hackers have lots of ways to compromise your password, and fast computers to try lots of passwords very quickly. There are plugins you can install to help reduce this risk (mentioned below) but the first line of defence is not to use bad usernames and passwords in the first place!

User names: Don’t use the same user name as display name. The display name is visible on blog entries, comments and posts so if the user name is the same, you’ve told a hacker what a valid user name on your site is and made their job a lot easier! Make them different. Also, avoid just using the account owner’s first name, last name or anything obvious. If a hacker discovers that Bob Smith owns the site (which is easy to do from Facebook or LinkedIn), they will try that as a user name in various forms.

I suggest something random like: happy-coder-9272 for Bob Smith would be ok.

Admin accounts: Never EVER call your admin account “admin”. Just don’t. Or “Administrator”, or SiteAdmin or anything which easy easy to guess. Even with a strong password it will be the target of the hackers efforts as it clearly has full permission so a valuable trophy should they manage to compromise it.

Passwords: Make sure your passwords are long. The longer they are, the longer it takes for an automated attack system to guess it as it takes exponentially more time to guess all combinations of a password for every extra character you add.

Avoid single word passwords, (even long ones) as they’re easy to attack with a dictionary attack.

Try a password that combines words into a sentence that means something to you, even including a few non-letter characters can boost its security massively. E.g. WordPress-needs-updating-Regul4rly is a strong password. Most account systems also allow spaces in passwords which can help.

3. Turn off Comments unless needed

If you don’t need people to be able to comment on your posts, turn it off. It’s on by default usually and gives a hacker an easy entry point into your system to try and hack it. You’ll also receive a lot of spam from Hot Lady Who’s Lonely, or someone in a foreign country who’s a lawyer for an unknown extremely deathly recently deceased relative.

4. Require SSL

If you aren’t aware of SSL, it’s a simple system built into every web browser that secures the traffic to and from your website. It automatically encrypts all requests to your site, and encrypts what your site sends back to the browser which can be checked for authenticity. If you’ve ever seen a web address start with https:// then it’s the “s” that means “secure” and uses SSL.

It’s transparent to you and pretty strong, which means an attacker can’t easily sit in between your browser and the website, pretending to be the other party to try and compromise the site. This is called a man-in-the-middle attack and is common.

The easiest way to turn on SSL is to install a plugin. There are a number of them which will do this, but for reasons I’ll mention next, I recommend the one we use which is Really Simple SSL.

This is a free plugin that will force all requests to use SSL and once activated will ask you to allow it to do this. If a user tries to access your site using http:// (without SSL security) it will automatically redirect them to https to make sure they access the site securely.

The plugin also provides some excellent security features which you really should turn on, but I’ll talk about that soon.

Note about preparing your site for SSL: When using SSL to access the site, if you have links in your site to images, documents or maybe even other sites that use http:// rather than https:// then you’re introducing a potential vulnerability. For content hosted on the same site, you just need to go to each link and change it to https instead and it should work. If you have links to external sites or content that are http:// you may need to see if they support https first (just try putting the “s” in the URL in your browser and see what happens).

5. Monitor Your Site

This may sounds strange, but you need to know if your site is being attacked and how so you can see if the security changes you’re making are actually making a difference. It will also help you understand if they’re a new vulnerability you’re exposed to. Any good admin will tell you how important monitoring your site is, and it’s easy to do.

WordPress by default doesn’t have monitoring built in. I have no idea why as it seems so important, but there are great plugins to do this for you. The one we use is:
Limit Login Attempts Reloaded

Once you activate this you’ll see a new widget on your dashboard and a new item in your settings where you can see a log of all those login attempts, and what happened. You may be surprised to see that for every legitimate login, there are 100 attempts to log in from somewhere in China or a strange unexpected country. As a WordPress site, without proper security in place, this is normal but at least you can see it for yourself.

A great feature of Limit Login Attempts is that they allow you to choose automatic lock-out rules, which is definitely worth setting up. This basically says that after 2-3 failed attempts to log in, you can block that connection (IP Address) from being able to try again for as long as you wish. 60 minutes (we have ours set to 2 hours). After this, any further failed logins will block them for 24 hours which is a great way to hamper the hacker attempts to use automated guessing bots to guess passwords.

If one of your legitimate users do accidentally lock themselves out, the plugin also has an easy way to unlock them immediately and you can also whitelist certain IP addresses which stops the lockout from happening at all.

6. More Advanced (but easy to implement) Hardening Techniques

Rename wp-admin

All WordPress pages have a default login page called “wp-admin”which makes it easy for a hacker to guess where they can target for login attempts. A recommended defence against this is to install a plugin that will rename the wp-admin page to something else so more difficult to find.

Search the plugins list for “rename wp-admin” and you’ll find some good ones, but 2 suggestions are:

Just remember to take a note or bookmark the new login page as it’ll be easy to forget!

Turn off XML-RPC

There are 2 main ways to get into your site from the “front door” (i.e. using a username and password login) – wp-login and XML-RPC. Both are legitimate routes but for different purposes.

  • wp-login is basically the wp-admin page (hopefully you’re renamed this by now) and allows someone to log in via their web browser. For manual attacks this is an easy route, but can also be subject to automated attacks. You need to leave this route alone or you won’t be able to log in to administer your site, but renaming it from the default wp-admin URL will help hide it and reduce the risk of compromise.
  • xml-rpc is used when you have systems that need to integrate into your site to automatically administer it (e.g. monitor it, automate certain workflows by other systems etc). This doesn’t have a web page login like wp-login, and is used by other systems to log in “in the background”. As this can most easily be used by an automated attack system, this is the route they usually use for brute force attacks. Unless you need this (or have a system that integrates to your WordPress site) then you’re safe to disable xml-rpc and it will reduce your attacks by 90%. You can turn this off using the Really Simple SSL plugin.

Other Hardening

Hardening means to increase the security of your site and the above will get you most of the way to a site that is difficult to compromise. The Really Simple SSL plugin we recommended also includes some easy-to-activate hardening options we suggest you turn on.

These options are under Settings > SSL > Settings tab (at the top) and Hardening in the Settings panel of this page (see above screenshot).

I’ve described what each does below:

  • Disable the built-in file editors – this stops hackers who are on your site from editing your content if you have plugins that allow this. Usually not so best keep this turned off.
  • Prevent code execution in the public Uploads folder – if your site allows files to be uploaded by your site users, this will make sure the file can’t be something that can be “run” (such as a script or executable). There’s almost no reason to ever have this turned off.
  • Hide your WordPress version – your WordPress version is relatively easy to discover on your site. Hackers use this information to find sites that haven’t been updated or may be using WordPress versions with known vulnerabilities that can be exploited. The less you can allow a hacker to discover about your site, the less ammunition you’ll be giving them to help get into your site.
  • Prevent login feedback – this is an interesting one. Usually you want to tell a user that they’ve entered a bad password or mistyped their user name to help your legitimate users. However, this can be useful to an attacker who is trying to compromise your site. If a bad login tells them that it was either their username or password that was wrong, that tells the hacker that the username exists (or doesn’t) so they know whether to focus on keep trying different usernames or focus on cracking the password if the username is a real one.
    It’s a balance between helping your users or helping the hackers. You need to decide for yourself whether to turn this on or not.
  • Disable Directory Browsing – web servers can be setup to allow the contents of a folder on your website to be listed to the user. Most often this is turned off as it can exploited by hackers to add or modify files in your site.
  • Disable User Enumeration – user enumeration is where an attacker uses various techniques to scan your site for legitimate user accounts so they can try to attack known accounts. This is obviously a bad thing but not difficult to do on WordPress sites. Turning this option on (to disable user enumeration) is highly recommended.
  • Block the username “admin” – you should never use “admin” for a user account. Turning this on stops you accidentally doing that and will offer to rename any accounts called “admin” that you may already have.
  • Disable XML-RPC – as described above, this turns off the XML-RPC support on the site so it can’t be used to attempt logins using automated attack tools.
  • Block user registrations when login and display name as the same – this is to support good practice and makes it harder for attackers to find valid user accounts they can try to attack.

The Really Simple SSL plugin also includes other security options which will increase your site security even further but they’re include in the paid upgrade version so won’t be covered here.

By using the above free tools, techniques and plugins, you will strengthen your website against attacks significantly and will be more than enough for most WordPress sites.

But remember, your security is only as good as your users. Despite how often we train our teams to use strong passwords and follow good security practices, your security is only as good as the weakest link.
One final piece of advice is to make sure you spend as much effort monitoring your internal users as the external ones if you want to remain secure.

If your site becomes compromised

A final note on how to deal with the unfortunate (and hopefully unlikely) event that your site is compromised, and how to detect this.

  • Your site contains strange content
  • Attempts to log into a user account that you’re sure you have the correct password for, is failing.
  • Your site is seeing an abnormal amount of outgoing traffic
  • Your site is really slow for no reason
  • Your site is showing 404 regularly (but not always)

The above are signs that your site may have been compromised, and either being used as a “hub” for an attacker to send their spam or malware from, or maybe that they’re using for their own purposes.

1. If you’re able to log into the site (wp-admin)

Once you can log into the site and access it’s setup, you should immediately do the following:

  1. Change the passwords of all user accounts. Make sure to choose something long and complicated for each, and make sure no 2 passwords are the same.
  2. Check for pages, posts or plugins that you didn’t install. Remove them all immediately.
  3. Update all plugins to the latest versions. If the attacker got into your site through a vulnerability in a plugin, you need to make sure the vulnerability is plugged if possible.
  4. Update your WordPress version if not using the latest one. This may include your PHP version.
  5. If you aren’t using the recommended security plugins mentioned in this article, now is a great time to install and configure them!

If the site is a mess due to what the attacker has done, you may need to restore a previous backup of your site first (to before it was compromised) and then do all 5 steps above to secure it and avoid it being compromised again.

2. If you aren’t able to log in

If your WordPress site is hosted by someone such as GoDaddy or Ionos, your first and best option is to contact their technical support team and ask them for help. They have tools and access to your site so will be able to recover it and help you track down how the attacker got into your site.

If you host the site yourself, you will need to use more advanced techniques but if you have backups of your site, you may want to restore it first so you can log in again and follow the steps above.

I hope this helps – happy Blogging!

Supporting the Text Scale setting in Delphi (iOS and Android)

In Delphi (Firemonkey), when you drop a TLabel onto a form, it will have a default font size of 12. Have you ever noticed that changing the size to 11 or 13 doesn’t give the results you expect, and both are usually smaller than when set to 12. Strange!

Is it a bug? Well no, it actually isn’t.

This is because Delphi treats font size 12 as a special value which isn’t necessary 12 pts in size. It’s best to think of FontSize: 12 as “default” instead and will apply the font scaling setting from your phone.

The problem is that the font scaling only applies when your font size is 12, so your app won’t honour the phone text scaling option for headings, captions etc which have smaller font sizes applied. Ideally you’d want all test within your UI to uniformly scale up or down as needed.

Luckily, it’s easy to ask the operating system for the scaling and applying it yourself.

{$IFDEF IOS}
uses iOSapi.UIKit;
{$ENDIF IOS}
{$IFDEF ANDROID}
uses Androidapi.JNI.GraphicsContentViewText;
{$ENDIF ANDROID}

function FontScale: Single;
{$IFDEF ANDROID}
var
  Resources: JResources;

  Configuration: JConfiguration;
{$ENDIF ANDROID}
{$IFDEF IOS}
var
  f: UIFontDescriptor;

{$ENDIF IOS}
begin

  Result := 1.0;

  {$IFDEF ANDROID}

  if TAndroidHelper.Context <> nil then

  begin

    Resources := TAndroidHelper.Context.getResources;
    if Resources <> nil then

    begin
      Configuration := Resources.getConfiguration;
      if Configuration <> nil then
        Result := Configuration.fontScale;

    end;

  end;

  {$ENDIF ANDROID}
  {$IFDEF IOS}
  f := TUIFontDescriptor.OCClass.preferredFontDescriptorWithTextStyle(
          UIFontTextStyleBody);
  Result := f.pointSize / 17.0;

  {$ENDIF IOS}
end;

Note: When designing your app and deciding on the font sizes for labels, you should do this relative to a scaling of 1.0 so they’re consistent. Applying the FontScale() function value above will turn these “logical” font sizes into “actual” font sizes which respect the phone’s font scaling setting.

The function returns a scaling value you could directly multiply your logical font size with before applying to your control to get the desired effect. E.g.:

label1.Font.Size:=14 * FontScale;

The end result will be a UI which respects the text scaling setting on the device.

Tip: For ease I recommend creating a helper function which returns the actual font size from the logical one, such as:

function LogToActualFontSize(const logicalSize: Single): Single;
begin
  Result:=logicalSize * FontScale;
end;

How does this work?

Android is fairly easy – it returns the actual scaling factor to apply. E.g. the default is 1.0, smaller may be 0.8 and larger may be 1.5. We can apply these directly.

iOS is a little trickier as we can’t get a scaling directly (for good reason). Instead, we get what we need by asking the type system for the current size of the body text style.
This will already be scaled by the text size setting, so we can simply divide it with the default body style font size at “normal” test sizing (17.0) to get the ratio – i.e. scaling factor.

Caveats and Things to be wary of…

The above will give you an easy way to achieve a scaling app but doesn’t address a few issues listed below. Note that I have no solution for these but if anyone discovers one, please add a comment.

  • Font Sizes of 12 will still apply the scaling regardless of the above, so you will want to ignore those cases to avoid double-scaling. Personally, I use a font.size of 12.1 to avoid this but you could put a “if logicalSize = 12 then return 12;” line in the helper function.
  • If the user changes the font scaling setting on their phone while the app is open in the background, the app won’t get notified of the change. There are ways to do this through observers but it’s beyond the scope of this article. See this post for more details:
    https://stackoverflow.com/questions/18951332/how-to-detect-dynamic-font-size-changes-from-ios-settings
  • Test very carefully for all text scale values! Your labels are likely to be a fixed size in your UI but your font size won’t stay the same as when it was designed so may introduce cropping or overflow issues.
  • If your font text doesn’t change then the most likely culprit will be the StyledSettings property of the label/control. By default, all labels will use the default font size set by the style unless the font size is changed at design time. If you’re setting the size at runtime instead, the new size will be ignored. Uncheck the Size option in this property to avoid Firemonkey ignoring the font size you set in code, or do the following in your code:

    label.StyledSettings:=label.StyledSettings – [TStyledSetting.Size];

I hope this helps you achieve a better behaving and more accessible user experience in your Delphi app.

For reference, the following articles were really useful in discovering how to do what’s described in this article:

https://programmersought.com/article/15291678656/

https://gist.github.com/zacwest/916d31da5d03405809c4

iOS Extensions with Firemonkey

Adding extensions to an iOS app is something that Delphi doesn’t support natively, but there is a way (with a few hurdles) to get this working in your Firemonkey app.

The first part of this article is based on this excellent Grijjy Blog article which I highly recommend reading: https://blog.grijjy.com/2018/11/15/ios-and-macos-app-extensions-with-delphi/

Introduction

First of all, I want to be clear that you can’t create extensions in Delphi (Pascal) directly, so you will need to learn enough Objective C to create them natively in Xcode. You’ll do this alongside a host app (also written in Objective C through Xcode) to test your extension. How you do this is completely up to you, and there are a lot of excellent tutorials, so Google away.

I’ve successfully built Delphi apps with accompanying Today and Share extensions (written natively) but the principle should be similar for other extension types.

Once you have a native app with a working extension in Xcode, you can extract the extension from the built app, and embed that into your Delphi Project.

All of the above is covered in the Grijjy blog so I will summarise it below, but if you’d like a better explanation as to how and why this is the case, please refer to the blog post.

If you’re creating a Share Extensions to allow other apps to share into your Delphi app, I recommend this post as an explanation of how to set up bundle groups and a simple working sharing app:

https://www.technetexperts.com/mobile/share-extension-in-ios-application-overview-with-example/

The Stages

I won’t go into detail here about how to create the Extension or share data between that and your Delphi app, as this is covered in the Grijjy blog and tutorial included above. I will however go into details above what you do next which isn’t covered by the above.

The problem is that you can build and include your plugin easily in your Delphi app, but it won’t actually run on your device.

Why? because Delphi only know about the app not the extension, so only includes one provisioning profile when signing. The problem is that your extension requires a separate profile, so the app needs building with two which Delphi doesn’t support.

If you try building, you will see errors like this:

[PAClient Error] Error: E0776 error: exportArchive: “<extension-name>.appex” requires a provisioning profile with the App Groups feature.

To resolve this, you need to take over the signing process from Delphi and make the necessary calls to xcodebuild with the correct information. A little fiddly but not too difficult.

This article attempts to take you through the process.

Note: I haven’t gotten this working for development builds, only release builds at the moment as the flow is different.
This means that the only way to test your Delphi + extension is to build for the App Store in Release mode and upload to TestFlight. This isn’t too bad if you’ve already tested your extension with a test (host) app in Xcode first, so make sure you do that or you’ll waste a lot of time uploading builds to TestFlight!

If I manage to get the following process working for the development builds too, I will update this article.

Building your Extension and Host App

First, build the app. The basic sequence is as follows:

  1. Use Xcode to build a native iOS app.

    Important – it MUST be created in Objective C not Swift. Delphi apps are effectively wrapped as Objective C apps so don’t play well with Swift extensions.
    Also Important – the bundle identifier of the host app MUST be identical to your Delphi app (if it already exists at this point). The host app you’re creating here is a test app which is pretending to be your Delphi app so you can test the extension and data-sharing between the two.
  2. Add an extension in Xcode (File > New > Target, then pick the extension type). The bundle identifier should be the same as the host app (step 1 above) but with a suffix specific to the extension. E.g. for an app “com.example.myapp” the extension should be “com.example.myapp.<extension_name>”.
  3. Build and test your extension in Xcode – make sure it works as expected.

    TIP: If you want to share data between your host app and extension (very likely), the most Delphi-compatible way is to create a bundle group, then use NSUserDefaults in both your extension and Delphi app to transfer data.
    How to do this on the Delphi side is included in the Grijjy blog, and how to do this from the Extension side is explained in the Share Extensions tutorial link.
  4. At this point you need to have a working Objective C app with an extension that is working and tested.

Provisioning Profiles Setup

If your Xcode host app and extension are using Xcode Managed Signing (which is very likely) then you will need to create development and deployment profiles manually and use them instead, as Delphi can’t access the Xcode managed profiles.

To find out if you’re using Xcode managed signing:

  1. In Xcode, tap the top item in your files tree on the left to show the project details on the right
  2. In the Signing & Capabilities tab, tap on each item in the Targets list. You will see a checkbox for “Xcode managed signing”. If they’re checked you need to do the steps below to create profiles manually and use them in Xcode.

    Manually creating your profiles

    Note: You will need to follow these stages twice – once for your main app identifier (e.g. com.example.myapp) and once for the extension (e.g. com.example.myapp.shareextension) as extensions have their own app ID and therefore need their own profiles.
  1. Open developer.apple.com, sign in and go into Certificates, IDs and Profiles
  2. In the Profiles section, tap the + button.
  3. Select App Store under the Distribution area.
  4. Select your app ID
  5. Follow the remaining stages, then create and download the certificate at the end.
  6. Double-click the downloaded file to install it into Xcode.
  7. Repeat the above for the extension app ID so you have 2 provisioning profiles, once for each.

Once you have your profiles:

  1. Open Xcode (if not already open)
  2. Tap on the top item in the files tree on the left to see the profile information details.
  3. You will have 2 items in the Targets area – once for the host app and one for your extension. You need to do the following for both targets:
  4. In the Signing & Capabilities tab, click the “Release” option in the toolbar (where it says “All, Debug, Release”) and uncheck the “Automatically manage signing” option.
  5. Select your team (it may be blank) and then select the correct provisioning profile from the list. The two you created in the developer website should appear here and be shown as “eligible”.
  6. Repeat for both targets.
  7. Build and run your app in Xcode again and make sure if still works correctly.
  8. Now you need to Archive the app within Xcode via the Product > Archive option
  9. After a short while the Organiser window will open showing your archived app (most recent at the top).
  10. Select the most recent archive file, then “Distribute App” button
  11. Choose “App Store” then Export (not Upload).
  12. Complete each of the following steps to get your archive file. When asked, make sure to uncheck “include bitcode…” and choose the correct distribution profiles for your app.

At this point you’re ready to transfer your extension into Delphi.

Extracting your Plugins Folder

To extract the Plugins folder, open the folder where you exported the archive to.

Note that you will need the ExportOptions.plist file later, so don’t lose it!

  1. Rename the .ipa file to .zip and extract it.
  2. In the Payload folder will be a .app file you need.
  3. Right-click on the app file and Show Package Contents
  4. Copy the Plugins folder to your PC inside your Delphi Project folder.

The Delphi Side

Once the Plugins folder is in your Delphi project folder, you need to include all of the files from the this folder in your Deployment within Delphi.

NOTE: On a Mac, the Plugins folder looks like it only contains one file – “Extension.appex” or similar. This is really a folder, and will show as such when on Windows. You need to make sure you include every file within the “.appex” folder and its subfolders in the distribution of your Delphi app or it won’t work!

The easiest way to do this is to use the DeployMan.exe tool kindly provided by Grijjy:

https://github.com/grijjy/DelphiSocialFrameworks/tree/master/DeployMan

Run this tool, choose “Import .dproj” from the menu and select the .dproj for your Delphi app, then add the entire Plugins folder into the list.

Make sure that the Target Directory is set to “./Plugins” and Include Subdirectories is checked.

Also, tap the Configurations option in the top-right and make sure you choose Debug if the extension was build for development in Xcode, or Release if build using the Archive technique.

The reason for this is because building for Archive or for development (the play button in Xcode) uses different provisioning profiles so they are different folders.

Deployman screenshot

Now File > Save Project and .dproj.

When you go back into your Delphi app it will detect the change and ask if you want to reload the project – say Yes!

Build and run your Delphi app. The bad news is that the extension still won’t work at the moment.

The trouble with profiles…

Delphi doesn’t support extensions for iOS app, which brings with it a big problem.

Extensions require separate provisioning profiles to the main app, so your app actually needs signing against two provision profiles, but Delphi only signs it against one as it doesn’t know any different.

If you try to perform a release build (for the App Store) through Delphi, it will generate an .app file containing everything you need but will be unable to sign it to generate the archive (.ipa) you will need to upload it to the App Store.

The error you will see will be something like this:

[PAClient Error] Error: E0776 error: exportArchive: "<extension-name>.appex" requires a provisioning profile with the App Groups feature.

[PAClient Error] Error: E0776 Error Domain=IDEProvisioningErrorDomain Code=9 ""<extension-name>.appex" requires a provisioning profile with the App Groups feature." UserInfo={IDEDistributionIssueSeverity=3, NSLocalizedDescription="<extension-name>.appex" requires a provisioning profile with the App Groups feature., NSLocalizedRecoverySuggestion=Add a profile to the "provisioningProfiles" dictionary in your Export Options property list.}

This is because it can only find a provisioning profile for the main app and not the extension.

For your app to work with an embedded extension, you will need to create a folder containing all the necessary files, then run xcodebuild manually to perform the archiving and signing process yourself on the .app file that Delphi generates.

The folder you are creating will have the same structure as a .xcarchive file (i.e. a temp subfolder and a Products subfolder into which your .app will be copied)

To perform the signing and archiving you need to create a folder containing 3 things:

  1. The .app file Delphi has generated ready for release (despite the error Delphi shows when building, the .app file is correct so copy it from your scratch folder)
  2. The exportOptions.plist file from the exported archive file described above which provides all the details about your app and references to the necessary provision profiles for both the app an extension.
  3. An info.plist file which is NOT the same as the ones generated in Delphi, so I’ve given details on this below.

For more info on the export procedure we’re following, see this article from Apple:
https://developer.apple.com/library/archive/technotes/tn2339/index.html#//apple_ref/doc/uid/DTS40014588-CH1-WHAT_KEYS_CAN_I_PASS_TO_THE_EXPORTOPTIONSPLIST_FLAG

The best approach is to create a shell script on the Mac to automate this, but that won’t be covered here.

Step 1 – get the .app file:

Build your app in Delphi for Release (App Store) if you haven’t already. It will fail with the error mentioned above, but that’s fine.

Next, open the scratch-dir folder on your Mac which Delphi builds into on the Mac. It’s usually here:

~/PAServer/scratch-dir/<name of Mac>

In here you will see the .app file that Delphi generated for your project.

Wherever you prefer on your Mac, create a folder with the same name as your app file but with a .archive extension (e.g. “MyApp.archive” if your app file was MyApp.app).

Create a Products folder within it and copy the MyApp.app file into the Products folder.

Copy the exportOptions.plist from the exported archive into the root of the .archive folder.

Create a Temp folder inside the .archive folder. This is where the .ipa for upload to the App Store will be generated if all goes well.

Create a plist.info file as described below in the root of the .archive folder.

Once you’re finished, the folder will look like this:

– MyApp.archive/
– info.plist
– ExportOptions.plist
– Products/
– MyApp.app
– Temp/

Creating the info.plist file you need:

Create a file called info.plist and copy in the following template:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ApplicationProperties</key>
<dict>
<key>ApplicationPath</key>
<string>myapp.app</string>
<key>CFBundleIdentifier</key>
<string>com.example.myapp</string>
<key>SigningIdentity</key>
<string>Apple Distribution: teamname (teamID)</string>
</dict>
<key>CreationDate</key>
<date>2019-02-26T17:05:43Z</date>
<key>Name</key>
<string>myapp</string>
</dict>
</plist>

Replace the <Name> value, the <ApplicationPath> value and the <CFBundleIdentifier> values with those for your app. The CreationDate seems not to matter.

The <SigningIdentity> key value needs to be the full (common) name of the distribution certificate (not provisioning profile) the app was generated using. this will be one associated with the provisioning profile, and the name should be similar structure to what I included above.
If unsure about this, download the distribution certificate file from the developer.apple.com portal (from the Certificates section of the portal), then select it in Finder and press the Spacebar to preview it. The name you need is shown in the preview window as Common Name.

TIP: If, when generating the archive (described below) you see errors relating to badly formed or missing “method” keys, it will be due to the info.plist file being wrong.

Generating the Archive (.ipa)

In a Terminal window, run the following command, replacing <scratch-dir> and <appname> as appropriate for your Mac and project:

/usr/bin/xcodebuild -exportArchive -archivePath "<scratch-dir>/<appname>.archive" -exportPath "<scratch-dir>/<appname>.archive/Temp" -exportOptionsPlist "<scratch-dir>/<appname>.archive/ExportOptions.plist"

The archive will be generated in the Temp folder, so at this point you can upload to Test Flight using Apple’s Transporter app.

I hope this was useful for all those who wish to run the gauntlet of Delphi and iOS extensions.

Happy Developing!

QUICK TIP: Accessing files in your iOS app bundle from Delphi

If you include files in your Firemonkey project with a remote path of ./ they’re copied into the main bundle when the app is built and deployed on iOS.

To access these files, you need to get their full path as follows:

Result:=NSStrToStr(TiOSHelper.MainBundle.pathForResource(StrToNSStr(fn), StrToNSStr(ext)));

fn = the name of the file without the extension

ext = the extension without the starting period (e.g. “caf’ not “.caf”)

If the file exists in the bundle it’s full path and filename will be returned, otherwise it’ll be an empty string.

You’ll need to include these units in your uses:

iOSapi.Helpers, Macapi.Helpers;

Happy coding!

Supporting In-App Purchase Subscriptions for iOS and Android Firemonkey Apps

NOTE: This article is relevant for Delphi 10.4.2 and earlier.
It is now a requirement to support Billing API v3 on Android which means an update in Delphi 11 has changed this. I will write an updated version of this blog for Delphi 11 soon.

The In-App Purchase support in Delphi is pretty well written and supports Consumable and Non-Consumable out-of-the-box.

Unfortunately, subscriptions aren’t supported by the built-in libraries but the good new is that it isn’t difficult to change the Firemonkey source files to make it work.

This tutorial takes you through the changes needed, along with how to check your subscription status and then how to get through Apple’s rigorous App Store Review process to actually get it onto the store.


If you aren’t already familiar with how to set up in-app purchases in general within your Delphi app, read these links:

http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Using_the_iOS_In-App_Purchase_Service

http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Using_the_Google_Play_In-app_Billing_Service


Pre-requisite: This tutorial isn’t a 101-how to use in-app purchases, and assumes that you’ve followed the above docwiki tutorials (which are really easy to use!) and are familiar with the basics.

Note: This tutorial is based on editing the 10.1 Berlin version of the files. I seriously doubt Embarcadero have updated them in Tokyo 10.2, but please check before making these changes just in case. Also note that you’ll need Professional or higher editions to make these changes as Starter doesn’t include the Firemonkey source files.

Some Background Reading…


The Android Subscription IAP reference is here:

https://developer.android.com/google/play/billing/billing_subscriptions.html


The iOS IAP references are here:

https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/Subscriptions.html


It iOS Receipt Validation Guides are here:

https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573

Let’s get started!

There are 2 workflows you need to add to your app in order to support subscriptions correctly:

  1. Purchase workflow
  2. Subscription status workflow (including activating or deactivating features accordingly)

The purchase workflow is fairly straightforward, but required you to make some changes to the in-built IAP classes provided with Delphi to support it.

The subscription status management workflow can be quite tricky as you’re required to check and handle everything yourself, but with some careful thought about this before starting to code, you should be able to offer a solid solution in your app which doesn’t create a jarring user experience.

Note: I’ll explain how to get access to the subscription status information but how you implement this within your own app logic is totally your choice. Some prefer to do the checking and feature expiry/activation on app load, others in different ways, so I won’t be offering any guidance around this in the tutorial as it will be different for every app.

1. Changes for the Purchase workflow

To start, we’re going to need to edit the following files:

  • FMX.InAppPurchase.pas
  • FMX.InAppPurchase.iOS.pas
  • FMX.InAppPurchase.Android.pas

These reside in the “Source\fmx” folder of your Delphi installation (on Berlin 10.1 this may be: c:\program files(x86)\embarcadero\studio\18.0\source\fmx)

Copy these files into your project folder, but don’t drag them into your project tree within the IDE. Just having them in your project folder will be enough for the compiler to find them and use them in preference to the system versions of the files.

FMX.InAppPurchase.pas:

We’re going to extend the base classes to add a few extra functions which you’ll need in order to produce a decent subscription experience for your users.

Note: in native iOS land, the calls for purchasing a subscription and consumable/non-consumable items is the same – it’s determined automatically by the bundle ID passed in. However, to provide a common interface that’s cross-platform, we added a separate call as you’ll see below that works for Android too.

In FMX.InAppPurchase.pas, make the following changes:

Within IFMXInAppPurchaseService, find:

procedure PurchaseProduct(const ProductID: string);

Add the following below it:

procedure PurchaseSubscription(const ProductID: string);
function GetProductToken(const ProductID: String): String;
function GetProductPurchaseDate(const ProductID: String): TDateTime;

Within TCustomInAppPurchase, search for the PurchaseProduct function and add the same new function headers as above.

Implement the new functions of TCustomInAppPurchase and copy the following code:

procedure TCustomInAppPurchase.PurchaseSubscription(const ProductID: string);
begin
  CheckInAppPurchaseIsSetup;
  if FInAppPurchaseService <> nil then
    FInAppPurchaseService.PurchaseSubscription(ProductID);
end;

function TCustomInAppPurchase.GetProductPurchaseDate(const ProductID: String): TDateTime;
begin
  if FInAppPurchaseService <> nil then
    Result := FInAppPurchaseService.GetProductPurchaseDate(ProductID)
  else
    Result:=EncodeDate(1970,1,1);
end;

function TCustomInAppPurchase.GetProductToken(const ProductID: String): String;
begin
  if FInAppPurchaseService <> nil then
    Result := FInAppPurchaseService.GetProductToken(ProductID)
  else
    Result:='';
end; 

FMX.InAppPurchase.iOS.pas:

Within TiOSInAppPurchaseService, find:

procedure PurchaseProduct(const ProductID: string);

Add the following below it:

procedure PurchaseSubscription(const ProductID: string);
function  GetProductToken(const ProductID: String): String;
function  GetProductPurchaseDate(const ProductID: String): TDateTime;

Implement these new functions using the code below:

procedure TiOSInAppPurchaseService.PurchaseSubscription(const ProductID: string);
begin
Self.PurchaseProduct(ProductID);
end;

function TiOSInAppPurchaseService.GetProductPurchaseDate(const ProductID: String): TDateTime;
begin
Result:=EncodeDate(1970,1,1);
end;

function TiOSInAppPurchaseService.GetProductToken(const ProductID: String): String;
begin
Result:='';
end;

As you can see, these are mostly dummy functions for iOS as this information isn’t available through the APIs, but is on Android.


So how do you get the purchase date and product token for this purchase? That’s where the receipt validation comes in! iOS doesn’t have native API calls to get this information about a subscription (unlike Android) so your app will need to cache the original purchase information at time of purchase, then rely on the receipt mechanism to update this cache at regular intervals in future.


I’ll go into this workflow later in the tutorial.

FMX.InAppPurchase.Android.pas:

Within TAndroidInAppPurchaseService, find:

procedure PurchaseProduct(const ProductID: string); 

Add the following below it:

procedure PurchaseSubscription(const ProductID: string);
function  GetProductToken(const ProductID: String): String;
function  GetProductPurchaseDate(const ProductID: String): TDateTime;

Implement these new functions using the code below:

procedure TAndroidInAppPurchaseService.PurchaseSubscription(const ProductID: string);
begin
  CheckApplicationLicenseKey;
  //Do NOT block re-purchases of products or users won't be able to
  //re-purchase expired subscription. Also makes it harder to test!
  //if IsProductPurchased(ProductID) then
  //  raise EIAPException.Create(SIAPAlreadyPurchased);
  //The docs say it must be called in the UI Thread, so...
  CallInUiThread(procedure
    begin
      FHelper.LaunchPurchaseFlow(ProductID, TProductKind.Subscription, InAppPurchaseRequestCode,
        DoPurchaseFinished, FTransactionPayload);
    end);
end;

function TAndroidInAppPurchaseService.GetProductPurchaseDate(const ProductID: String): TDateTime;
  var
    Purchase: TPurchase;
begin
  Result:=EncodeDate(1970,1,1);
  if FInventory.IsPurchased(ProductID) then
  begin
    Purchase:=FInventory.Purchases[ProductID];
    if Purchase <> nil then
      Result:=Purchase.PurchaseTime;
  end;
end;

function TAndroidInAppPurchaseService.GetProductToken(const ProductID: String): String;
  var
    Purchase: TPurchase;
begin
  Result:='';
  if FInventory.IsPurchased(ProductID) then
  begin
    Purchase:=FInventory.Purchases[ProductID];
    if Purchase <> nil then
      Result:=Purchase.Token;
  end;
end;

With these changes, you’ll be able to access the most recent transactionID for a product purchase (useful to display in a purchase history UI) and the last purchase date.

There are a few “bug fixes” you need to apply within the Android unit too:

procedure TInventory.AddPurchase(const Purchase: TPurchase);
begin

  //For subscription it must replace or it'll keep the old transactionID 
  if IsPurchased(Purchase.Sku) then

    ErasePurchase(Purchase.Sku);

  FPurchaseMap.Add(Purchase.Sku, Purchase);
end;

The original code caches the transaction ID and dates internally, so whenever a subscription is re-purchased (e.g. during testing or if the user changes their mind about cancelling a subscription) it won’t keep the new transaction ID. The above change fixes this by replacing the purchase details.

And also:

procedure TAndroidInAppPurchaseService.DoPurchaseFinished(const IabResult: TIabResult; const Purchase: TPurchase);
begin
  if IabResult.IsSuccess then

  begin
    //If we've a payload on the purchase, ensure it checks out before completing
    if not Purchase.DeveloperPayload.IsEmpty then

    begin
      if not DoVerifyPayload(Purchase) then
      begin
        DoError(TFailureKind.Purchase, SIAPPayloadVerificationFailed);

        Exit;

      end;

    end;
    FInventory.AddPurchase(Purchase);

    //These two are swapped to match the event call order of iOS
    DoRecordTransaction(Purchase.Sku, Purchase.Token, Purchase.PurchaseTime);
    DoPurchaseCompleted(Purchase.Sku, True);
  end
  else
    DoError(TFailureKind.Purchase, IabResult.ToString);
end;

This fix is important as the Android code calls RecordTransaction and PurchaseComplete in a different order to iOS for some reason, which makes it hard to write logic which handles the subscription code easily.

It’s worth mentioning how the TransactionID and PurchaseDate behave, so you can decide how best to use them:


Non-Consumable Purchases:

Both are the ORIGINAL purchase date and transactionID for the purchase.


Subscriptions:

Both are the MOST RECENT re-purchase date and transactionID for the subscription. When a subscription is automatically renewed, a new transactionID is generated and the purchase date will update to reflect when it was renewed.


For Android, you can then use the purchase date to work out the expiry date for your user experience. On iOS, you need to use the receipt to get this.

Note: There is much better and recommended way to get the latest status of your user purchases, which is to listen to Real Time Developer Notifications (RTDN) and App Store Server Notification on your server and update the purchase records on your own server.

This is more work and outside the scope of this tutorial, but for a more robust and accurate way to handle subscriptions, you really should look into doing this. It also allows you to detect subscription upgrades/downgrades, paused and resumed subscriptions (Android) and more situations that you can’t get from just the app-level APIs.

For details, take a look here:

https://developer.android.com/google/play/billing/getting-ready

https://developer.android.com/google/play/billing/rtdn-reference

https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/enabling_app_store_server_notifications



At this point, you will be able to purchase subscriptions for Android and iOS through the updated API. This is only the first part of the challenge! Now we need to check for expiry…

2. Checking Subscription Status

Subscription status checking isn’t as easy as the one-call approach to purchasing, as there aren’t any API calls to give you this information to the level of detail you’ll need.


To get details about the user’s subscription you can do 1 of 2 things depending on the level of detail you require:


1. Just call .IsProductPurchased() on the IAP class.


This will return true/false but may not be as reliable as you think. There as known cases where the store returns “yes” when the subscription has actually been cancelled or expired. This also won’t tell you when the subscription is due to expire so should only used used as a high level check.


2. Use Receipt Validation (iOS) or subscribe to Real-Time Developer Notifications (Android).


This will provide you with real up-to-date information about the subscription, when it’s expected to expire, what really happened to it, why and when.


Note: RTDN (Android) MUST be done from your server as it uses a webhook back to your server – effectively poking your server when something has changed, that you can then use to update your back-end records about the subscription.


For Receipt Validation, this CAN be done from within your app, but Apple recommend you do this from your server too.

Checking for Expiry

The Quick Way – iOS or Android


Using IsProductPurchased() may be enough if all your want to do is disable a feature if the store thinks that the user’s subscription is no longer active.

Subscriptions can be reported as inactive in the following situations:

  • The subscription has expired and isn’t auto-renewable
  • The subscription renewal payment could not be taken (and any payment grace period has expired)
  • The subscription was cancelled by the user, so wasn’t auto-renewed
  • The subscription was made on another device, or the app has been re-installed, and the user hasn’t yet done a Restore Purchases (iOS only).

Note: this isn’t the recommended way to check for subscription expiry, as there are too many ways it can report the status falsely, which can lead to you removing features from paying users – which users don’t tend to like!

Also, you may have problem retaining users as the basic system described above has no way to detect when someone has cancelled the renewal of their subscription, or if they’re in a trial period, having billing issues or other cases where you may way to provide some UI to try to persuade or help the user to stay on your subscription.

The BETTER way – Android

Google offer a push-based notification system, which tells you when something changes about a subscription so you can update your records and act accordingly.


If you have a server on which you keep a record of your user’s purchases, then you can create relevant end points in your server API which can be called by Google’s servers when the status of a subscription changes. This gives you a LOT more information, especially about why it’s no longer active, but will only work if your server keeps a record or has a way to push an IAP status change to your app (e.g. using Push Notifications).


These are called Real Time Developer Notifications and there are instructions in the relevant section here:

https://developer.android.com/google/play/billing/billing_subscriptions.html#realtime-notifications

Along with Developer RTN, your server can also link to the Google Developer APIs which give you the ability to ask Google directly for details about your users purchases (by purchase token). This approach gives you a lot of details about the purchase, it’s status, reason why it was cancelled or has expired, billing retry, trial periods etc.

It’s worth pointing out that there is a chance your server won’t pick up all the RTDN messages for lots of legitimate reasons, or may not be able to process them all in time before your app asks for the latest status.
This is what makes implementing the full subscription workflow so complicated – and not just for Delphi, for any app. Google’s advice is to also use Google Developer APIs to supplement and double-check the server records you’re keeping.

It’s quite a complex implementation requiring you to create and link to a Google Cloud API, set up Pub/Sub and link your Play account but if you’re up for the challenge, it’s a great solution. For details, take a look here:

https://developers.google.com/android-publisher

The BETTER way – iOS

For iOS, Apple offer a receipt validation REST API which you can call to get full details about the purchases made by the user.

Details of how this works is here:

https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104

This requires you to send a binary-encoded version of the user’s purchase receipt to their server, along with your API key (generated in the iTunes Connect page where you created your subscription iAP). In return you’ll receive a large JSON object which is the unencrypted version of the receipt with everything you need to know about all purchases the user has made within your app.


The above link gives you an idea of the structure you can expect and what the receipt fields include (and format) so you can process these.


But what is the user purchase receipt and where can I find it?


When a user makes a purchase, a receipt of this is added to a receipt file which is encrypted with the user’s key. The receipt file is stored in the sandbox for the app, so if the app is deleted, so is the receipt file.


That’s great, but if the user re-installs the app, the receipt won’t be there!


This is where the Restore Purchases facility comes in, and why you MUST implement it in your app for iOS. When the user restores their purchases, the app is given a new receipt file, which it then stores in the app sandbox.


It’s this receipt file that is used when you query IsProductPurchased() which is why, if you don’t call Restore Purchases, your app will clam that your user doesn’t have the purchases they’ve paid for.


To find your receipt file within your app, do the following:

mainBundle:=TiOSHelper.MainBundle;
receiptUrl:=mainBundle.appStoreReceiptURL;  
urlAsString:=NSStrToStr(receiptUrl.path); 


You will need to include the following in your uses:

iOSapi.Foundation,
iOSapi.Helpers,
Macapi.Helpers;


Note: You can’t just read this file and get what you need from it, as it’s encrypted against the user’s private key, so you will need to send it to Apple’s validation server, which will decrypted and send you the ready-to-use JSON version of the receipt.


A Delphi-based example of how you may choose to read the file and send to Apple is below:

if TFile.Exists(urlAsString) then</p>
begin
  streamIn:=TFileStream.Create(urlAsString, fmOpenRead);
  streamOut:=TStringStream.Create;
  try
    TNetEncoding.Base64.Encode(streamIn, streamOut);
    receiptBase64:=streamOut.DataString.Replace(#10, '', [rfReplaceAll])
      .Replace(#13, '', [rfReplaceAll]);
    if urlAsString.Contains('sandbox') then<
      baseUrl = 'https://sandbox.itunes.apple.com/'
    else
      baseUrl = 'https://buy.itunes.apple.com/';

    RESTRequest1.Method:=TRestRequestMethod.rmPost;
    RESTClient1.BaseURL:=baseUrl;
    RESTRequest1.Resource:='verifyReceipt';
    RESTRequest1.AddBody('{"receipt-data": "'+receiptBase64+'", '+
        ' "password": "'+cSecretKey+'"}');

    RESTRequest1.Execute;
  finally
    streamIn.Free;
    streamOut.Free;
  end;
end;


NOTE: Apple suggest you don’t do this call from within your app, and instead should do it from your server –  though it’s the same process.


cSecretKey is the shared secret that you need to generate within the iTunesConnect page of your subscription IAP product. It’s only required if you have auto-renewing subscriptions within your app.


In our app’s implementation, we base64 encrypt the receipt file as shown above, but instead of sending directly to Apple, we POST it to our server instead, which then sends onto Apple, and can decode and process the purchase receipt as required in our app’s behalf.


The Resulting Receipt Data

The response you get back from Apple should be the JSON data of the receipt, which is in the “receipt” field of the JSON response.


However, if an error has occurred, the “status” field will tell you what went wrong, so you’re best to test this before assuming the receipt field is valid. A status of “0” means that all went well and your receipt field has been populated. Anything else is an error which you can look up the meaning for in here:

https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104


Tip: The most common errors are 21002 which means that your receipt data couldn’t be decoded and may not have been ready correctly from the file. Occasionally it will return this even if your data is correct – maybe if you’ve run a receipt check call twice in quick succession from your app.

The other we had to begin with was 21007 which means you’ve accidentally sent a sandbox receipt file to the live URL or vice-versa.


A Note about Environments

The App Store has 2 areas which affect in-app purchases – Sandbox and Live.

When in Sandbox mode (i.e. when you’re logged into the App Store using a Sandbox User account created through the iTunesConnect portal), you won’t be charged for any purchases made, and subscriptions will have an artificially shortened lifespan so you can test expiry conditions much more easily.

The full receipt validation sandbox URL is:

https://sandbox.itunes.apple.com/verifyReceipt

Note about sandbox subscriptions: Once you’ve purchased a subscription using a specific sandbox account, it will renew 5-6 times (once per hour) then expire. Once expired, you can’t make another purchase of the same subscription under that account, so if you want to test again, you’ll need to create a new sandbox account in iTC.

It’s a bit of a pain, but just what you have to do!

When in Live mode (i.e. you’re logged in with a live AppleID – NOT recommended during testing), all purchases will be charged for, so best to do this once you’re happy your code works as a final test.

The full receipt validation live URL is:

https://buy.itunes.apple.com/verifyReceipt

A Note about Receipts…

As mentioned earlier, receipts are pulled onto local storage when you restore purchases, but when handling subscription you are likely to want refresh the receipt quite regularly to make sure you have the latest information (such as expiry dates).

There is nothing in the InAppPurchase libraries which do this (the receipt refresh when calling RestorePurchases is built into the Apple SDKs), however, it’s not difficult to do:

//Import the necessary missing calls from StoreKit SDK
SKReceiptRefreshRequestClass = interface(SKRequestClass)

    ['{903AA321-6DC7-49D8-88C0-B393C563958B}']
end;

SKReceiptRefreshRequest = interface(SKRequest)
    ['{B26069D9-26BA-4BF4-9ECB-B9AD4B2A4AEE}']
  function initWithReceiptProperties(properties: NSDictionary)
: Pointer; cdecl;

  function receiptProperties: NSDictionary; cdecl;
end;


TSKReceiptRefreshRequest = class
    
(TOCGenericImport<SKReceiptRefreshRequestClass, SKReceiptRefreshRequest>)
 
end;

procedure TPurchaseReceiptManager.RefreshReceipt(OnComplete: TProc);
  var

    request: SKReceiptRefreshRequest;
begin
  request:=TSKReceiptRefreshRequest.Wrap(
      TSKReceiptRefreshRequest.Alloc.initWithReceiptProperties(nil));

  if request <> nil then
    request.start;
end;

A note on the above code – it will make a call to ask Apple for the latest receipt for that user from their store purchases, but it may take a moment for the actual receipt file to refresh and be available.

An easy (if basic) solution would be to call the above in a thread, sleep for a couple of seconds then call the code to get the receipt.
A much better solution would be to create an SKRequestDelegate-implementing class which you can pass to SKReceiptRefreshRequest.delegate. This will then tell you when your refresh has completed.
For a similar example to see how you can do this in Delphi, take a look at TiOSProductsRequestDelegate in the EMB sources (FMX.InAppPurchase.iOS.pas).

3. Getting App-Store Ready

So now you should be able to offer your users the ability to purchase a subscription, and get details about it’s expiry, cancellation etc in order to manage access to your feature.

You may now think that you’re able to implement the purchase UX in your app however you choose… but I’m afraid it’s not that simple.

Google seem fairly lenient on the requirements, as long as you make it clear what the user is buying and for how much. Apple on the other hand, have strict requirements for how you present your purchase facility and what details you show to the user about the subscription prior to them purchasing it.

And be under no illusions – Apple will reject your app if you don’t have EXACTLY the right things in your app they’re looking for.

However, they aren’t being deliberately awkward, they simply want to make sure the subscriber isn’t under any illusion as to what they’re buying, how and when they’ll be charged and in what capacity.

When we first submitted our app for review, it was rejected on a few points that at first baffled us. The rejection from Apple was:

We noticed that your App did not fully meet the terms and conditions for auto-renewing subscriptions, as specified in Schedule 2, Section 3.8(b).

Please revise your app to include the missing information. Adding the above information to the StoreKit modal alert is not sufficient; the information must also be listed somewhere within the app itself, and it must be displayed clearly and conspicuously.

Eventually we working it out thanks to a combination of the posts below, but to make life easier, I’ve included the specific requirements below:

https://forums.developer.apple.com/thread/74227

https://support.magplus.com/hc/en-us/articles/203808548-iOS-Tips-Tricks-Avoiding-Apple-Rejection

The key things to focus on are:

  • You need very specific wording in both your purchase page for the subscription, and also within the App details text on the store listing. The latter confused us as Apple kept saying it must be in the iAP description but that can only take a 30 characters! We eventually realised they were referring to the app store listing itself.
  • The purchase page for your subscription MUST include a link to your Privacy Policy and Terms.
  • It’s recommended that you offer a direct way to access the subscription management portal from within your app. The URLs for these are:

        For iOS:

https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/manageSubscriptions

        For Android:

https://play.google.com/store/account/

The wording you need to include against your subscription in the in-app purchase page is as follows. Note that this is just a guide and you may be asked by Apple to add extra details based on your specific subscription or content type.

1. This subscription offers ……………………………….

Make this as descriptive as possible. e.g. if you offer different subscription levels, then mention it.

2. The subscription includes a free trial period of …. which begins as soon as you purchase the subscription. You can cancel your subscription at any time during this period without being charged as long as you do so within 24 hours of the end of trial period.

3. Payment will be charged to iTunes account at confirmation of purchase

4. Your subscription will automatically renew unless auto-renew is turned off at least 24-hour prior to the end of the current period

5. Your account will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal

6. Your subscriptions can be managed by the user and auto-renewal may be turned off by going to the user’s Account Settings after purchase.

7. No cancellation of the current subscription is allowed during the active subscription period

8. Privacy Policy link goes here

And there we have it! The above should be all you need to implement support for auto-renewable subscriptions in your Firemonkey Android and iOS app.

9. Terms of Use link goes here.

We hope you found this useful. Happy coding!

Final Notes and Food for Thought…

Subscription implementation is quite complex, but the good news is that it’s the same problem regardless of which tools you use for your app and back-end. It’s the logic that’s complex because of the sheer range of different use-cases you might need to deal with.

My suggestion on this is to Google how others have implemented their subscription management logic and go with whichever is comprehensive enough for your situation. You don’t need to do a perfect job of supporting every case, but the more you can handle, the more likely you are to keep users subscribed and paying you.

Here are a few area you need to look into, and try to support if possible:

  • Users upgrading / downgrading between different types of subscription in a subscription group (e.g. switching from Annual to Monthly)
  • Family Sharing (iOS) – this is where one person in a family group buys a subscription or in-app purchase and allows others in their defined family group to get access to them. Good news is that the receipt validation should pick this up properly, but it’s worth reading up on it and testing it yourself.
  • Billing issues, Re-try periods and grace periods – these are where a payment couldn’t be taken but the user has been given some time to get it fixed before you take their subscribed features from them.
    You can detect this from the receipt / RTDN / Google Developer API calls so you can provide an appropriate API.
  • Server-Side Purchase History – you can try to do everything from within your app, and in some ways the logic is easier, but it is less secure so both Apple and Google ask you not to do this. Validating receipts, talking to the Apple/Google servers to validate things – none of them will protect you from common man-in-the-middle attacks so best to implement your own server which you push purchase info to and use as your single source of truth. This is basically what both Apple and Google Recommend. It also allows you to use RTDN to keep those up-to-date.
  • Cancellation and Expiry – make sure you test these cases properly! Not just so you can remove features when needed, but also to avoid removing then re-activating features unnecessarily between expiry/renew periods if there’s a small gap due to server records not being updated on time!

In all, it’s a mine-field but if you get it working it’s a very worthwhile way to make some real money from your app.