YaniPay Watch
Application compagnon pour Apple Watch. Consultez votre solde, payez en NFC depuis votre poignet et restez connecte a vos finances grace aux complications et notifications intelligentes.
Overview
YaniPay Watchest une application compagnon concu pour l'Apple Watch Series 9 et ulterieur. Elle fonctionne en tandem avec l'application iPhone via WatchConnectivity pour offrir un acces rapide a vos finances directement depuis votre poignet.
L'application exploite les capacites natives de watchOS 11+ : complications riches sur les cadrans, paiement NFC Tap-to-Pay, synchronisation bi-directionnelle en temps reel et deep linking pour une navigation instantanee.
<100ms
Sync latency
AES-256
Encryption
13
Complications
Application Compagnon
YaniPay Watch necessite l'application iPhone YaniPay installee et configuree. La Watch app se synchronise automatiquement via WatchConnectivity et fonctionne de maniere autonome pour les operations de lecture (solde, historique) meme sans connexion iPhone.
Configuration Requise
| Composant | Minimum | Recommande |
|---|---|---|
| Apple Watch | Series 9 | Series 10 / Ultra 2 |
| watchOS | 11.0 | 11.5+ |
| iPhone compagnon | iPhone 15 (iOS 18.5+) | iPhone 16 Pro |
| Xcode | 16.0 | 16.2+ |
| Swift | 5.9 | 6.0 |
| NFC (Tap-to-Pay) | Apple Watch Series 9+ | Ultra 2 (portee etendue) |
Fonctionnalites Cles
YaniPay Watch est structure autour de quatre piliers fonctionnels, chacun optimise pour l'experience au poignet avec des interactions rapides et un acces instantane.
Balance & Transactions
Consultez votre solde en temps reel et l'historique de vos transactions directement depuis votre poignet. Actualisation automatique via WatchConnectivity.
- Solde multi-devises (EUR, YANI, BTC)
- Historique 30 derniers jours
- Pull-to-refresh & background refresh
Tap-to-Pay
Payez en NFC sans contact directement depuis votre Apple Watch. Integration PassKit native avec tokenisation securisee Secure Enclave.
- NFC contactless via PassKit
- Double-clic bouton lateral pour payer
- Tokenisation Secure Enclave
Complications
Affichez vos donnees financieres sur les cadrans Apple Watch. Support de toutes les familles de complications watchOS 11.
- Solde actuel sur le cadran
- Nombre de transactions du jour
- Tendance +/- sur 7 jours
Deep Linking
Navigation directe vers les ecrans cles via URL schemes. Permet le lancement rapide depuis Siri, Shortcuts et complications.
- yanipay://payment
- yanipay://transactions
- yanipay://loyalty
- yanipay://qrcode
Synchronisation
La synchronisation entre l'iPhone et l'Apple Watch repose sur le framework WatchConnectivity. Le serviceWatchSyncService gere la communication bi-directionnelle avec un mecanisme de retry exponentiel et un cache local pour le mode hors-ligne.
import WatchConnectivity
/// Bi-directional sync service for iPhone <-> Apple Watch communication
class WatchSyncService: NSObject, WCSessionDelegate {
static let shared = WatchSyncService()
private var session: WCSession?
private var retryCount = 0
private let maxRetries = 5
// MARK: - Initialization
override init() {
super.init()
if WCSession.isSupported() {
session = WCSession.default
session?.delegate = self
session?.activate()
}
}
// MARK: - Bi-directional Sync
/// Sync all data with exponential backoff retry
func syncData() async throws {
guard let session, session.isReachable else {
// Fallback to application context (queued delivery)
try await syncViaApplicationContext()
return
}
do {
// iPhone -> Watch: balance, transactions, loyalty cards
let context: [String: Any] = [
"balance": try await fetchBalance(),
"transactions": try await fetchRecentTransactions(),
"loyaltyCards": try await fetchLoyaltyCards(),
"lastSync": Date().timeIntervalSince1970
]
try await session.sendMessage(context)
retryCount = 0 // Reset on success
} catch {
retryCount += 1
if retryCount <= maxRetries {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
let delay = pow(2.0, Double(retryCount - 1))
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
try await syncData()
} else {
retryCount = 0
throw WatchSyncError.maxRetriesExceeded
}
}
}
// MARK: - Watch -> iPhone (Checkout Requests)
/// Handle payment request from Watch
func session(_ session: WCSession,
didReceiveMessage message: [String: Any],
replyHandler: @escaping ([String: Any]) -> Void) {
guard let action = message["action"] as? String else { return }
switch action {
case "checkout":
// Watch -> iPhone: process payment request
let amount = message["amount"] as? Double ?? 0
let currency = message["currency"] as? String ?? "EUR"
processCheckout(amount: amount, currency: currency) { result in
replyHandler(["status": result.status, "txId": result.transactionId])
}
case "quickAction":
// Watch -> iPhone: trigger quick action
let actionType = message["type"] as? String ?? ""
handleQuickAction(actionType) { response in
replyHandler(response)
}
default:
replyHandler(["error": "Unknown action"])
}
}
// MARK: - Deep Linking
/// Handle deep links from complications & Siri
func handleDeepLink(_ url: URL) {
// yanipay://payment?amount=50
// yanipay://transactions?id=TX123
// yanipay://loyalty?cardId=CARD123
// yanipay://qrcode
guard let host = url.host else { return }
let params = URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?
.reduce(into: [String: String]()) { $0[$1.name] = $1.value }
switch host {
case "payment":
navigateToPayment(amount: params?["amount"], currency: params?["currency"])
case "transactions":
navigateToTransactions(id: params?["id"])
case "loyalty":
navigateToLoyalty(cardId: params?["cardId"])
case "qrcode":
navigateToQRCode()
default:
break
}
}
}Synchronisation Bi-Directionnelle
Mode Hors-Ligne
Lorsque l'iPhone n'est pas a portee, l'Apple Watch utilise son cache local Core Data pour afficher les dernieres donnees synchronisees. Les requetes de paiement sont mises en file d'attente et traitees automatiquement a la reconnexion via transferUserInfo().
extension WatchSyncService {
/// Fallback sync via application context (queued delivery)
func syncViaApplicationContext() async throws {
guard let session else { throw WatchSyncError.sessionUnavailable }
let context: [String: Any] = [
"balance": CacheManager.shared.lastKnownBalance,
"transactions": CacheManager.shared.cachedTransactions,
"timestamp": Date().timeIntervalSince1970
]
// Application context is delivered when Watch becomes reachable
try session.updateApplicationContext(context)
}
/// Queue payment request for later processing
func queuePaymentRequest(_ request: PaymentRequest) {
// transferUserInfo guarantees delivery (FIFO queue)
session?.transferUserInfo([
"action": "checkout",
"amount": request.amount,
"currency": request.currency,
"recipient": request.recipient,
"queuedAt": Date().timeIntervalSince1970
])
}
}Deep Linking
YaniPay Watch supporte les deep links via le scheme yanipay://pour permettre une navigation directe depuis les complications, Siri Shortcuts et les notifications push.
| URL Scheme | Description | Parametres |
|---|---|---|
yanipay://payment | Ouvrir l'ecran de paiement rapide | amount, currency, recipient |
yanipay://transactions | Afficher l'historique des transactions | id, filter, dateRange |
yanipay://loyalty | Acceder aux cartes de fidelite | cardId |
yanipay://qrcode | Afficher le QR code de reception | amount (optional) |
import SwiftUI
import WatchKit
@main
struct YaniPayWatchApp: App {
@WKApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
// Handle deep links from complications & Siri
WatchSyncService.shared.handleDeepLink(url)
}
}
}
}
// Info.plist URL Scheme:
// <key>CFBundleURLTypes</key>
// <array>
// <dict>
// <key>CFBundleURLSchemes</key>
// <array><string>yanipay</string></array>
// </dict>
// </array>Siri Shortcuts Integration
Les deep links sont automatiquement enregistres comme Siri Shortcuts via NSUserActivity. L'utilisateur peut dire "Hey Siri, paiement YaniPay" pour ouvrir directement l'ecran de paiement sur la Watch.
Complications
Les complications YaniPay permettent d'afficher les donnees financieres directement sur le cadran de la montre. L'application prend en charge 13 familles de complications, couvrant tous les types de cadrans watchOS 11.
| Famille | Donnees affichees | Exemple | Taille |
|---|---|---|---|
circularSmall | Solde abrege | 2.4K | 36x36 pt |
modularSmall | Solde + tendance | 2,450 EUR +3% | 52x52 pt |
modularLarge | Solde + 3 dernieres TX | Balance + mini-list | 52x170 pt |
utilitarianSmall | Solde compact | 2,450 | 40x40 pt |
utilitarianSmallFlat | Solde + devise | 2,450 EUR | Inline text |
utilitarianLarge | Solde complet + label | YaniPay: 2,450.00 EUR | Full width |
graphicCorner | Gauge solde + icone | Gauge circulaire | Corner slot |
graphicCircular | Ring progress + solde | Ring avec montant | 47x47 pt |
graphicRectangular | Solde + sparkline 7j | Mini chart + balance | 157x69 pt |
graphicExtraLarge | Dashboard complet | Solde + TX + tendance | Full face |
accessoryCorner | Icone + solde | Ultra cadran | Corner slot (Ultra) |
accessoryRectangular | Solde + dernieres TX | Widget-style | Wide rect |
accessoryInline | Texte solde inline | 2,450.00 EUR | Single line |
import WidgetKit
import SwiftUI
struct YaniPayComplication: Widget {
let kind = "com.yanipay.complication"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: ComplicationProvider()) { entry in
ComplicationView(entry: entry)
}
.configurationDisplayName("YaniPay")
.description("Solde et transactions en un coup d'oeil")
.supportedFamilies([
// ClockKit Legacy
.accessoryCircular,
.accessoryRectangular,
.accessoryInline,
.accessoryCorner,
// WidgetKit (watchOS 10+)
.graphicCorner,
.graphicCircular,
.graphicRectangular,
.graphicExtraLarge,
])
}
}
struct ComplicationProvider: TimelineProvider {
typealias Entry = BalanceEntry
func placeholder(in context: Context) -> BalanceEntry {
BalanceEntry(date: .now, balance: 2450.00, currency: "EUR", trend: .up)
}
func getSnapshot(in context: Context, completion: @escaping (BalanceEntry) -> Void) {
let entry = BalanceEntry(
date: .now,
balance: CacheManager.shared.lastKnownBalance,
currency: "EUR",
trend: CacheManager.shared.balanceTrend
)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<BalanceEntry>) -> Void) {
Task {
let balance = try await WatchSyncService.shared.fetchBalance()
let entry = BalanceEntry(
date: .now,
balance: balance.amount,
currency: balance.currency,
trend: balance.trend
)
// Refresh every 15 minutes
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: .now)!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
}
struct BalanceEntry: TimelineEntry {
let date: Date
let balance: Double
let currency: String
let trend: BalanceTrend
enum BalanceTrend {
case up, down, stable
}
}
struct ComplicationView: View {
@Environment(\.widgetFamily) var family
let entry: BalanceEntry
var body: some View {
switch family {
case .accessoryCircular:
ZStack {
AccessoryWidgetBackground()
VStack(spacing: 0) {
Image(systemName: "eurosign.circle.fill")
.font(.title3)
Text(entry.balance.formatted(.number.precision(.fractionLength(0))))
.font(.caption2)
.fontWeight(.bold)
}
}
case .accessoryRectangular:
VStack(alignment: .leading, spacing: 2) {
Text("YaniPay")
.font(.caption2)
.foregroundStyle(.secondary)
Text(entry.balance.formatted(.currency(code: entry.currency)))
.font(.headline)
.fontWeight(.bold)
HStack(spacing: 4) {
Image(systemName: entry.trend == .up ? "arrow.up.right" : "arrow.down.right")
.font(.caption2)
Text("7 derniers jours")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
case .graphicRectangular:
VStack(alignment: .leading) {
Text("Solde YaniPay")
.font(.caption2)
.foregroundStyle(.secondary)
Text(entry.balance.formatted(.currency(code: entry.currency)))
.font(.title3)
.fontWeight(.bold)
// Mini sparkline chart would go here
}
default:
Text(entry.balance.formatted(.number.precision(.fractionLength(0))))
.font(.caption)
}
}
}Taux de Rafraichissement
Les complications se rafraichissent toutes les 15 minutes via TimelineProvider. Pour les mises a jour instantanees (nouveau paiement recu), utilisez WidgetCenter.shared.reloadAllTimelines() depuis le handler WatchConnectivity.
Notifications
L'Apple Watch recoit des notifications push categoriees avec des actions interactives, permettant de valider ou refuser un paiement directement depuis le poignet.
Transaction Recue
Notification haptic + montant + expediteur. Tap pour ouvrir le detail.
Validation Paiement
Boutons Approuver / Refuser pour les paiements au-dessus du seuil configure.
Fidelite
Notification de proximite (geofencing) pour presenter la carte de fidelite automatiquement.
import UserNotifications
extension UNNotificationCategory {
static var yanipayCategories: [UNNotificationCategory] {
// Payment approval category
let approveAction = UNNotificationAction(
identifier: "APPROVE_PAYMENT",
title: "Approuver",
options: [.authenticationRequired]
)
let rejectAction = UNNotificationAction(
identifier: "REJECT_PAYMENT",
title: "Refuser",
options: [.destructive, .authenticationRequired]
)
let paymentCategory = UNNotificationCategory(
identifier: "PAYMENT_APPROVAL",
actions: [approveAction, rejectAction],
intentIdentifiers: [],
options: .customDismissAction
)
// Transaction received category
let viewAction = UNNotificationAction(
identifier: "VIEW_TRANSACTION",
title: "Voir le detail",
options: .foreground
)
let transactionCategory = UNNotificationCategory(
identifier: "TRANSACTION_RECEIVED",
actions: [viewAction],
intentIdentifiers: [],
options: []
)
return [paymentCategory, transactionCategory]
}
}Build & Deploy
L'application watchOS est construite comme une target WatchKit dans le projet Xcode YaniPay. Voici les commandes pour compiler et tester sur le simulateur.
# Build watchOS app for simulator
xcodebuild build \
-workspace YaniPay.xcworkspace \
-scheme "YaniPay Watch App" \
-destination 'platform=watchOS Simulator,name=Apple Watch Series 9 (45mm)' \
-configuration Debug
# Run tests
xcodebuild test \
-workspace YaniPay.xcworkspace \
-scheme "YaniPay Watch App" \
-destination 'platform=watchOS Simulator,name=Apple Watch Series 9 (45mm)'
# Build for device (requires signing)
xcodebuild build \
-workspace YaniPay.xcworkspace \
-scheme "YaniPay Watch App" \
-destination 'generic/platform=watchOS' \
-configuration Release \
CODE_SIGN_IDENTITY="Apple Distribution" \
DEVELOPMENT_TEAM="YOUR_TEAM_ID"
# Archive for App Store
xcodebuild archive \
-workspace YaniPay.xcworkspace \
-scheme "YaniPay Watch App" \
-archivePath build/YaniPayWatch.xcarchive \
-destination 'generic/platform=watchOS'Provisioning Profile
Le Tap-to-Pay NFC requiert un entitlement com.apple.developer.nfc.readersession.formats specifique et un provisioning profile configure via le portail Apple Developer. Contactez l'equipe devops pour la configuration des certificats.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<!-- NFC Tap-to-Pay -->
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
<string>TAG</string>
</array>
<!-- HealthKit (optional - spending habits tracking) -->
<key>com.apple.developer.healthkit</key>
<false/>
<!-- Background Modes -->
<key>com.apple.developer.background-modes</key>
<array>
<string>remote-notification</string>
<string>background-fetch</string>
</array>
<!-- Keychain Sharing (shared with iPhone app) -->
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.yanipay.shared</string>
</array>
<!-- App Groups (shared data with complications) -->
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.yanipay.watch</string>
</array>
</dict>
</plist>Architecture
L'application watchOS suit l'architecture MVVM avec des @Observable view models (Swift 5.9+). Elle consomme le package YaniPayCore partage avec l'application iPhone pour les models, protocols et services metier.
Structure du Projet watchOS
import Foundation
import YaniPayCore
@Observable
final class BalanceViewModel {
var balance: Double = 0.0
var currency: String = "EUR"
var recentTransactions: [Transaction] = []
var isLoading: Bool = false
var error: WatchSyncError?
private let syncService = WatchSyncService.shared
private let cache = CacheManager.shared
// MARK: - Data Loading
func loadData() async {
isLoading = true
defer { isLoading = false }
// Load from cache first (instant UI)
balance = cache.lastKnownBalance
recentTransactions = cache.cachedTransactions
// Then sync from iPhone
do {
try await syncService.syncData()
balance = cache.lastKnownBalance // Updated by sync
recentTransactions = cache.cachedTransactions
error = nil
} catch let syncError as WatchSyncError {
error = syncError
// UI still shows cached data
}
}
// MARK: - Formatted Display
var formattedBalance: String {
balance.formatted(.currency(code: currency))
}
var balanceTrend: String {
let trend = cache.balanceTrend
switch trend {
case .up: return "+\(cache.trendPercentage)%"
case .down: return "-\(cache.trendPercentage)%"
case .stable: return "~0%"
}
}
}Next Steps
Application iOS
L'application iPhone compagnon requise pour la Watch
YaniPayCore Framework
Package Swift partage entre iPhone et Watch
Integrations Apple
Apple Pay, PassKit, NFC et Siri Shortcuts
Securite
Secure Enclave, Keychain et chiffrement CryptoKit
Battery Optimization
La batterie de l'Apple Watch est une contrainte fondamentale du developpement watchOS. Chaque operation reseau, chaque acces capteur et chaque refresh de complication consomme un budget energetique limite. YaniPay watchOS est concu pour maximiser l'autonomie tout en offrant des donnees financieres actualisees.
// Background refresh budget: ~4 per hour
// Complication updates: timeline-driven, not polling
// Network: use URLSession background tasks
// Sensor data: batch readings, avoid continuous
import WatchKit
import BackgroundTasks
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
scheduleBackgroundRefresh()
}
func scheduleBackgroundRefresh() {
// Budget: ~4 background refreshes per hour
let fireDate = Date().addingTimeInterval(15 * 60) // 15 min
WKExtension.shared().scheduleBackgroundRefresh(
withPreferredDate: fireDate,
userInfo: nil
) { error in
if let error {
print("Background refresh scheduling failed: \(error)")
}
}
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for task in backgroundTasks {
switch task {
case let refreshTask as WKApplicationRefreshBackgroundTask:
// Batch all updates in one pass
Task {
await WalletManager.shared.refreshBalance()
await ComplicationUpdater.shared.updateTimeline()
refreshTask.setTaskCompletedWithSnapshot(false)
scheduleBackgroundRefresh() // Re-schedule
}
default:
task.setTaskCompletedWithSnapshot(false)
}
}
}
}Pratiques recommandees
- +Timeline-driven complications (pas de polling)
- +Batch les lectures capteurs en une seule session
- +URLSession background tasks pour le reseau
- +Compresser les donnees WatchConnectivity
A eviter
- -Polling reseau continu en background
- -Capteurs actifs en permanence (gyroscope, heart rate)
- -Animations complexes en arriere-plan
- -Transferts de donnees volumineux non compresses
Budget de rafraichissement
watchOS alloue un budget dynamique de rafraichissement en arriere-plan base sur les habitudes d'utilisation. Plus l'utilisateur consulte l'application frequemment, plus le systeme lui accorde de refreshes. Priorisez les mises a jour lors des moments habituels d'usage (matin, dejeuner, soir).
Health & Fitness Integration
watchOS s'integre nativement avec HealthKit et les Activity Rings d'Apple. YaniPay peut exploiter ces donnees pour proposer des correlations originales entre activite physique et comportements financiers, ou pour enrichir l'experience lors des sessions sportives.
Correlations HealthKit
Correlation optionnelle entre depenses alimentaires et calories brulees. Donnees strictement anonymisees et traitees on-device. Aucune donnee sante transmise aux serveurs YaniPay.
Activity Rings
Concept d'integration : recompenses YANI lorsque l'utilisateur ferme ses anneaux d'activite. Programme de fidelite base sur les habitudes saines, avec accord explicite de l'utilisateur.
Workout Payment Tracking
Suivi des depenses liees au sport pendant une session Workout. Tap-to-pay en mode sportif avec interface simplifiee resistante a la sueur et aux mouvements brusques.
Confidentialite des donnees sante
L'integration HealthKit est strictement optionnelleet necessite un consentement explicite de l'utilisateur via la boite de dialogue systeme Apple. YaniPay ne transmet aucune donnee de sante hors de l'appareil. Toutes les correlations sont calculees localement avec les frameworks Apple on-device.
References
Documentation officielle Apple pour le developpement watchOS et les technologies utilisees par YaniPay Watch.