Docs

The library

Gating paid features

Sell the whole app with Paywall.gate, or some features with Paywall.start and one check per feature. This page lists every public call and constant in the library, and how trials work.

Whole app or some features

Paywall.gate(views)Paywall.start(views)
Use it forAn app paid for as a wholeAn app that sells some features
At launch, lockedThe buy screen in your app's placeYour app, as usual
During a trialYour app, as usualYour app, as usual
When the trial endsThe buy screen at each launch, titled "Trial over"The buy screen once, titled "Trial over"
BACK on the buy screenLeaves the appGoes to your app
Once paidBACK or START opens your viewsBACK or START goes to your app
Each paid featureNothing to addChecks Paywall.unlocked() first

Both take the same array you'd return from getInitialView(): a view, or a view and its delegate.

Gating a feature

With start, check where each paid feature begins: a button, a menu item, a page.

source/MainDelegate.mc
function onHistory() as Void {
    if (Paywall.unlocked()) {
        showHistory();
    } else {
        Paywall.open();
    }
}
  • One lock opens everything. One purchase opens every paid feature in the app. There is one price per app.
  • In a glance, call only active(), unlocked() and trialLeft(). The buy screen is app-only.
  • Thank-you touches that should show only to buyers, not during a trial, check Paywall.active().

Reference

Everything here lives in module Paywall (Pay.mc and the PayScope.mc you use), unless the table says otherwise.

Calls you make

CallReturnsWhat it doesWorks in
Paywall.gate(views)The views to returnIn getInitialView(), for an app paid for as a whole. Starts the trial, asks the server when it's time, and while locked returns the buy screen in your app's place. Once paid, BACK or START opens your views.App
Paywall.start(views)The views to returnIn getInitialView(), for an app that sells some features. Starts the trial, asks the server when it's time, and returns your views. Shows the buy screen once when the trial ends.App
Paywall.unlocked()BooleanPaid, or in the trial (all of it, before the trial starts). The check for every paid feature.App, glance
Paywall.active()BooleanPaid, and confirmed by the server within the last 30 days. False during a trial. A "paid" stamp more than 5 minutes in the future (a clock set back) doesn't count.App, glance
Paywall.trialLeft()NumberWhole days of trial left, rounded up. Never more than TRIAL_DAYS. All of TRIAL_DAYS before the trial starts; 0 once it's over, or with no trial.App, glance
Paywall.open()PaywallViewPushes the buy screen, or returns the one already showing. For a paid feature's else, and a "Premium" item in your settings.App
Paywall.keys()ArrayThe storage keys your "Delete data" must keep: "pp", "ts" and "te".App
Paywall.startTrial()VoidOlder call. Starts the time trial at first launch; nothing without TRIAL_DAYS. gate and start call it.App
Paywall.launch()VoidOlder call. Asks the server when its time has come, or at once if the watch's clock has gone back. gate and start call it.App
Paywall.showOver()VoidOlder call, from your first screen's onShow(). From then on, a purchase made away from the watch opens the unlock by itself; with a trial, it opens the buy screen once when the trial ends. gate and start do this for you.App

Called by the library itself

The library calls these itself. Don't call them from your code.

FunctionWhat it does
check()Sends one ask to the server, never two at once. An ask with no answer for a minute stops counting.
onAnswer(code, data)Handles the server's reply. Keeps only a whole answer, one with a version and a next check. On a failure, sets "Offline" or "Try later" and the wait before the next try.
take(d)Stores an answer, the trial's end and "paid". Opens the unlock when a purchase lands.
first(views)The shared part of gate and start.
close()Leaves the buy screen: to your views once paid (or after start's trial-end screen), out of a locked gate app, or back to where open() was called.
cheer()The watch's success tone and a buzz, if the watch has them on.

Constants

ConstantValueWhat it is
K_ANS"pa"Storage key: the server's last answer.
K_PAID"pp"Storage key: when the server last said "paid", in seconds. None: not paid.
K_TRIAL"ts"Storage key: when the trial ends. Set at first launch, then by the server.
K_OVER"te"Storage key: the "Trial over" screen has shown.
STALE259200030 days in seconds: how long "paid" counts without a new answer from the server.
DAY86400A day in seconds.

Module variables

The buy screen's own state. Read them if you need to; never set them.

VariableWhat it holds
listenerThe open buy screen, or null.
statusThe problem line ("Offline" or "Try later"), or null.
busy, sentAtAn ask is out, and when it went.
hold, heldHow long the server may hold an ask while the buy screen shows (15 seconds at first, halved if the phone cuts it short).
changedAt, limitWhen the screen last changed, and how many seconds it may stay open without a change.
fails, nextAtAsks failed in a row, and when the next may go.
shownYour app's first screen is up: a purchase may open the unlock by itself.
quickThe buy screen has opened: its first ask comes back at once.
app, wholeYour views while the buy screen stands in for them, and whether gate was used.

Classes

ClassWhat it is
PaywallViewThe buy screen. It draws the server's screen and plays the unlock.
PaywallDelegateThe buy screen's buttons: START and BACK.
PayReplyThe web request's callback object.
PayBgFaces and data fields only: the background service that asks the server.

PaywallConfig

ConstantWhat it is
URLRequired. Your app's address, from your dashboard.
TRIAL_DAYSApps: optional, leave it out for no trial. Faces and data fields: required, 0 for no trial.

Watch faces and data fields

Faces and data fields use lib/watch/face, whose module Paywall has its own calls. active(), unlocked() and trialLeft() work as above.

CallWhat it does
Paywall.startTrial()Every start, from getInitialView(): starts the trial the first time, and lets a finished sign-in page wake the background.
Paywall.schedule(false)Every start, from getInitialView(): books the next background check.
Paywall.take(d)From onBackgroundData(d): stores the background's answer and books the next check.
Paywall.soon()From onSettingsChanged(): a check as soon as Garmin allows, and the sign-in notification again while locked.
Paywall.drawQr(dc, x, y, side)Draws the QR code side wide on a white square, centered at x, y. Nothing before the first answer.
Paywall.book(n)Called by the library: books a background check, never sooner than 5 minutes after the last.
K_SIGN"pg": storage key for the sign-in notification (1 to send, 2 sent).

The whole setup is on Watch faces and data fields.

Trials

  • Set TRIAL_DAYS in PaywallConfig, the same as your dashboard's free trial. Leave it out for no trial: then "Trial over" never shows.
  • The trial starts at first launch. Paywall.unlocked() is true until it ends.
  • Reinstalling never restarts it. Our server counts the trial from the first time it saw that watch, and its count replaces the watch's at the first answer.
  • A clock set back can't stretch it. The server sends time left, not a date, and the watch never counts more days than TRIAL_DAYS.
  • The buy screen's title shows "7 days left", then "Last day", then "Trial over".

A trial your app counts

For a trial by use instead of days, leave TRIAL_DAYS out, keep your own counter in storage (and through "Delete data"), and check active():

source/MainDelegate.mc
// 20 free sessions, then Premium
if (Paywall.active() || sessionsUsed < 20) {
    startSession();
} else {
    Paywall.open();
}

What the library stores

  • Apps: "pa" (the answer), "pp" (when "paid" was last confirmed), "ts" (the trial's end) and "te" ("Trial over" shown).
  • Faces and data fields: "pa", "pp", "ts" and "pg", plus the unlock property for the settings link.
  • Use other names for your own keys.
Esc