Docs

The library

Watch faces and data fields

A watch face can't take button presses or reach the network on screen. So the library checks with our server in the background, your face draws the QR code where you choose, and Garmin's sign-in notification lets a buyer pay without scanning. Data fields work the same way.

How it works

  • The background asks. The face's background service sends our server the same request an app does, at the time the server gave.
  • The time stays on top. On a face, viewing the time is a primary feature, and Garmin advises no paywall over it. While locked, your face draws the QR code where you choose. A data field draws it in its own space.
  • The link is in the settings. The face also writes its buy link into its settings, which the Connect IQ app on the phone shows. A buyer can open it there without scanning.
  • The notification skips the scan. Once the face is locked and the phone is connected, it sends Garmin's sign-in notification. The buyer taps it, pays, and the face unlocks within seconds.

Add it to a face or data field

Faces and data fields use lib/watch/face only: PayFace.mc and PayFaceScope.mc.

monkey.jungle
base.sourcePath = source;../wearcheckout/lib/watch/face
manifest.xml
<iq:permissions>
    <iq:uses-permission id="Background"/>
    <iq:uses-permission id="Communications"/>
</iq:permissions>

The config is annotated (:background), since the background reads it. Keep TRIAL_DAYS in it, and set it to 0 for no trial: the face's part reads it directly.

source/PaywallConfig.mc
// PaywallConfig.mc: from your dashboard (Trail Face). Public, not a secret.
import Toybox.Lang;

(:background)
module PaywallConfig {
    const URL = "https://pay.example/api/K7Q2/c";   // your app's address
    const TRIAL_DAYS = 7;
}

The hooks

WhereCall
getInitialView()Paywall.startTrial(); Paywall.schedule(false); on every start
getServiceDelegate()return [new PayBg()];
onBackgroundData(data)Paywall.take(data);
onSettingsChanged()Paywall.soon();
onUpdate(dc)if (!Paywall.unlocked()) { Paywall.drawQr(dc, x, y, side); }

A whole face, with the time on top and the code under it while locked:

source/PayFaceApp.mc
import Toybox.Application;
import Toybox.Background;
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.System;
import Toybox.Time;
import Toybox.WatchUi;

// A small face for measuring: the time, and the QR code under it while locked.
(:background)
class PayFaceApp extends Application.AppBase {
    function initialize() {
        AppBase.initialize();
    }

    function onStart(state as Dictionary or Null) as Void {
    }

    function getInitialView() {
        Paywall.startTrial();
        Paywall.schedule(false);
        return [new PayFaceView()];
    }

    function getServiceDelegate() {
        return [new PayBg()];
    }

    function onBackgroundData(data) as Void {
        Paywall.take(data);
    }

    // Saving the face's settings on the phone (the buy page suggests it after paying): ask now
    // (5 minutes after the last check at the soonest, Garmin's rule).
    function onSettingsChanged() as Void {
        Paywall.soon();
    }
}

class PayFaceView extends WatchUi.WatchFace {
    function initialize() {
        WatchFace.initialize();
    }

    function onUpdate(dc as Graphics.Dc) as Void {
        var w = dc.getWidth();
        var h = dc.getHeight();
        var t = System.getClockTime();
        dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
        dc.clear();
        dc.drawText(w / 2, h * 30 / 100, Graphics.FONT_NUMBER_MEDIUM, t.hour.format("%02d") + ":" + t.min.format("%02d"),
            Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
        if (!Paywall.unlocked()) {
            Paywall.drawQr(dc, w / 2, h * 68 / 100, w * 36 / 100);
        }
    }
}

Add a read-only unlock property. The face fills it in with the buy link from the server's answer. Define the UnlockTitle string in your own resources.

resources/settings/settings.xml
<resources>
    <!-- The buy link, written by the face from the server's answer: the phone's Connect IQ app
         shows it in this face's settings, so a buyer can open it without scanning. -->
    <properties>
        <property id="unlock" type="string"></property>
    </properties>
    <settings>
        <setting propertyKey="@Properties.unlock" title="@Strings.UnlockTitle">
            <settingConfig type="alphaNumeric" readonly="true"/>
        </setting>
    </settings>
</resources>

Drawing the code

  • Paywall.drawQr(dc, x, y, side) draws the code side wide on a white square, centered at x, y. Before the first answer it draws nothing.
  • Call it only while !Paywall.unlocked().
  • Draw it only with the screen fully on, never in an AMOLED watch's always-on mode.

Garmin's sign-in notification

  • When it's sent: once, the first time a check finds the face locked (its trial over, or no trial). Again each time the buyer saves the face's settings while it's locked. Only while the phone is connected: it needs the phone itself, not Wi-Fi.
  • What the buyer sees: Garmin's own notification on the phone. Garmin writes its words ("<app> needs you to sign in"), and no app can change them. Tapping it opens your checkout in Garmin Connect's own browser.
  • After paying: the page ends, the phone hands the result to the watch, and the face's background asks our server. The result alone unlocks nothing, since anyone could type its address. Only our server's answer unlocks.
  • Never sent twice by mistake: the background marks it sent itself (CIQ 3.2 and later), even if the face doesn't run soon after. On a watch that can't send it, the QR code and the settings link still work.

Apps don't send it. An app's buy screen already unlocks about a second after paying.

How long an unlock takes

Our server can't reach a watch. A face learns of a payment only at its next check, and Garmin allows a background check at most every 5 minutes.

How the buyer paidThe face unlocks
Tapped the sign-in notificationWithin seconds
Scanned the code or used the settings link, in the 2 days after install or the trial's endWithin 10 minutes
The same, laterWithin an hour
Saved the face's settings on the phone after payingAt a check as soon as Garmin allows: 5 minutes after the last one at the soonest
  • Once a check has seen the checkout opened, the face checks every 5 minutes for half an hour.
  • During the trial, the face checks at the trial's end, or daily if that's sooner.
  • After a failed check (no phone), the next is an hour later.

Annotations and scope

Glance and background code can reach only code annotated for them. Keep the library's annotations as they ship, and match your own.

  • Faces and data fields: PaywallConfig is (:background), and so is your AppBase class, as in the sample: its getServiceDelegate() runs in the background. PayBg and PayFaceScope.mc are already annotated.
  • Apps with a glance: glance/PayScope.mc and PaywallConfig are (:glance). A glance may call Paywall.active(), Paywall.unlocked() and Paywall.trialLeft(), nothing else.
  • Apps with their own background service: glance-bg/PayScope.mc, PaywallConfig as (:glance, :background), and the Background permission.
  • App-only: everything in Pay.mc, including Paywall.open() and the buy screen and its strings. Never call it from a glance or background code.
Esc