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.
base.sourcePath = source;../wearcheckout/lib/watch/face<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.
// 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
| Where | Call |
|---|---|
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:
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);
}
}
}The settings link
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>
<!-- 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 codesidewide on a white square, centered atx,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 paid | The face unlocks |
|---|---|
| Tapped the sign-in notification | Within seconds |
| Scanned the code or used the settings link, in the 2 days after install or the trial's end | Within 10 minutes |
| The same, later | Within an hour |
| Saved the face's settings on the phone after paying | At 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:
PaywallConfigis(:background), and so is yourAppBaseclass, as in the sample: itsgetServiceDelegate()runs in the background.PayBgandPayFaceScope.mcare already annotated. - Apps with a glance:
glance/PayScope.mcandPaywallConfigare(:glance). A glance may callPaywall.active(),Paywall.unlocked()andPaywall.trialLeft(), nothing else. - Apps with their own background service:
glance-bg/PayScope.mc,PaywallConfigas(:glance, :background), and the Background permission. - App-only: everything in
Pay.mc, includingPaywall.open()and the buy screen and its strings. Never call it from a glance or background code.