Push Notification & Deep Link #

Dalam ekosistem aplikasi seluler modern, push notification (notifikasi push) dan deep link (tautan mendalam) adalah dua pilar teknologi yang sangat krusial untuk meningkatkan keterlibatan pengguna (user engagement) dan retensi aplikasi. Notifikasi push bertindak sebagai pemanggil eksternal yang menarik perhatian pengguna kembali ke aplikasi kita, sementara deep link bertindak sebagai pemandu arah yang memastikan pengguna langsung diarahkan ke halaman spesifik yang relevan (seperti halaman detail produk promo atau status pengiriman pesanan) saat mereka mengetuk notifikasi tersebut—bukan sekadar membuka halaman beranda utama.

Integrasi kedua teknologi ini membutuhkan pemahaman yang mendalam mengenai lapisan native platform (FCM di Android, APNs di iOS), sinkronisasi daur hidup status aplikasi (app lifecycle states), dan arsitektur routing deklaratif di sisi Flutter.

Arsitektur Pengiriman Notifikasi dan Deep Linking #

Alur kerja pengiriman notifikasi dari server backend kita hingga menjadi aksi navigasi di layar pengguna Flutter melewati beberapa tahapan arsitektur berikut:

  1. Pemicuan Server (Backend): Server kita mengirimkan muatan data (payload JSON) ke gerbang push milik platform target (FCM untuk Android dan APNs untuk iOS).
  2. Transmisi Gateway: FCM/APNs mengirimkan notifikasi tersebut ke perangkat pengguna melalui koneksi soket persisten yang dikelola oleh sistem operasi latar belakang.
  3. Respon Sistem Operasi: Perangkat menerima notifikasi. Jika aplikasi berada di background atau terminated, OS akan menggambar notifikasi di bilah status (system tray). Jika aplikasi di foreground, pesan langsung diserahkan ke aplikasi.
  4. Tindakan Pengguna & Routing: Ketika pengguna mengetuk notifikasi, OS meluncurkan atau membangkitkan aplikasi kita dengan menyertakan payload data. Router aplikasi kita (misalnya GoRouter) menerjemahkan payload data tersebut menjadi parameter navigasi halaman.

Integrasi Firebase Cloud Messaging (FCM) & APNs #

Untuk mengaktifkan fitur notifikasi push di Flutter, kita menggunakan package resmi firebase_messaging yang dipadukan dengan flutter_local_notifications untuk menangani rendering notifikasi secara kustom ketika aplikasi sedang aktif di foreground.

1. Instalasi Dependensi #

Tambahkan pustaka berikut di dalam file pubspec.yaml kita:

dependencies:
  firebase_core: ^3.8.1
  firebase_messaging: ^15.1.6
  flutter_local_notifications: ^17.2.4

2. Inisialisasi Firebase & Penjadwalan Latar Belakang #

Gunakan FlutterFire CLI untuk melakukan konfigurasi proyek Firebase secara otomatis pada target platform Android dan iOS kita:

# Jalankan konfigurasi Firebase via CLI
flutterfire configure --project=nama-proyek-firebase-kita

Pada iOS, pastikan kita mengaktifkan kapabilitas Push Notifications dan Background Modes (centang Remote notifications) di Xcode pada bagian Signing & Capabilities. Kita direkomendasikan menggunakan kunci otentikasi APNs (.p8) di Firebase Console karena kunci ini tidak memiliki masa kedaluwarsa dan dapat digunakan untuk beberapa aplikasi sekaligus.


Penanganan Notifikasi pada Tiga State Daur Hidup Aplikasi #

Perilaku penerimaan notifikasi bervariasi tergantung pada status daur hidup (lifecycle state) aplikasi kita saat pesan tersebut tiba. Berikut adalah visualisasi alur perutean data notifikasi berdasarkan status aplikasi:

flowchart TD
    Start["Notifikasi Diterima Perangkat"] --> CheckState{"Bagaimana Status Aplikasi?"}
    
    CheckState -- "1. Foreground (App Terbuka)" --> FG_Receive["FCM Menerima Pesan (onMessage)"]
    FG_Receive --> FG_Local["Tampilkan Notifikasi Manual via flutter_local_notifications"]
    FG_Local --> FG_Tap["Pengguna Tap Notifikasi"]
    FG_Tap --> FG_Route["Tafsirkan JSON Data & Arahkan Rute dengan GoRouter"]

    CheckState -- "2. Background (App Minimized)" --> BG_Receive["OS Menampilkan Notifikasi di System Tray"]
    BG_Receive --> BG_Tap["Pengguna Tap Notifikasi"]
    BG_Tap --> BG_Handler["FCM Memicu callback onMessageOpenedApp"]
    BG_Handler --> BG_Route["Ekstrak Data Payload & Navigasi Halaman"]

    CheckState -- "3. Terminated (App Mati)" --> TM_Receive["OS Menampilkan Notifikasi di System Tray"]
    TM_Receive --> TM_Tap["Pengguna Tap Notifikasi"]
    TM_Tap --> TM_Launch["OS Meluncurkan Aplikasi (Cold Start)"]
    TM_Launch --> TM_Check["Panggil getInitialMessage() setelah Router Siap"]
    TM_Check --> TM_Route["Ekstrak Data Payload & Alihkan ke Rute Spesifik"]

Berikut adalah implementasi lengkap kelas layanan NotificationService yang menangani ketiga skenario status daur hidup tersebut secara aman dan modular:

// lib/core/notifications/notification_service.dart
import 'dart:convert';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../router/app_router.dart';

/// Handler background message wajib ditempatkan sebagai top-level function
/// dan dianotasikan dengan @pragma('vm:entry-point') agar tidak dihapus compiler.
@pragma('vm:entry-point')
Future<void> _fcmBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  debugPrint('[FCM Background] Menerima pesan: ${message.messageId}');
}

class NotificationService {
  static final FirebaseMessaging _messaging = FirebaseMessaging.instance;
  static final FlutterLocalNotificationsPlugin _localNotifications =
      FlutterLocalNotificationsPlugin();

  static const AndroidNotificationChannel _channelPenting = AndroidNotificationChannel(
    'channel_penting_id', 
    'Notifikasi Penting',
    description: 'Channel ini digunakan untuk notifikasi transaksi & promo penting.',
    importance: Importance.max,
    playSound: true,
  );

  static Future<void> inisialisasi() async {
    // 1. Registrasi Background Handler di baris awal
    FirebaseMessaging.onBackgroundMessage(_fcmBackgroundHandler);

    // 2. Minta Izin Notifikasi (terutama penting untuk iOS dan Android 13+)
    final NotificationSettings settings = await _messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );
    debugPrint('Status Izin Notifikasi: ${settings.authorizationStatus}');

    // 3. Konfigurasi Local Notification untuk Foreground
    const AndroidInitializationSettings inisialisasiAndroid =
        AndroidInitializationSettings('@mipmap/ic_launcher');
    const DarwinInitializationSettings inisialisasiIOS = DarwinInitializationSettings(
      requestAlertPermission: false,
      requestBadgePermission: false,
      requestSoundPermission: false,
    );

    await _localNotifications.initialize(
      const InitializationSettings(android: inisialisasiAndroid, iOS: inisialisasiIOS),
      onDidReceiveNotificationResponse: _onTapNotificationLokal,
    );

    // 4. Buat Channel Notifikasi Android
    await _localNotifications
        .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
        ?.createNotificationChannel(_channelPenting);

    // 5. Daftarkan Event Handler
    _setupEventHandlers();

    // 6. Ambil Token FCM untuk Pengiriman dari Backend
    final token = await _messaging.getToken();
    debugPrint('FCM Token Perangkat: $token');
    if (token != null) {
      await _kirimTokenKeServer(token);
    }

    // Perbarui token di server jika terjadi rotasi token
    _messaging.onTokenRefresh.listen(_kirimTokenKeServer);
  }

  static void _setupEventHandlers() {
    // KONDISI 1: FOREGROUND (Aplikasi Sedang Terbuka Aktif)
    // OS tidak akan menampilkan banner secara otomatis. Kita harus memicu notifikasi lokal.
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      debugPrint('[FCM Foreground] Pesan masuk: ${message.notification?.title}');
      _tampilkanNotifikasiLokal(message);
    });

    // KONDISI 2: BACKGROUND (Aplikasi Terbuka tapi Sedang Diminimalisasi)
    // OS menampilkan banner di tray. Callback ini terpicu HANYA saat pengguna mengetuk banner.
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      debugPrint('[FCM Background Tap] Pengguna mengetuk notifikasi.');
      _arahkanRuteHalaman(message.data);
    });

    // KONDISI 3: TERMINATED (Aplikasi dalam Keadaan Mati Total)
    // Kita panggil saat inisialisasi untuk memeriksa apakah aplikasi dinyalakan dari tap notifikasi.
    _periksaNotifikasiColdStart();
  }

  static Future<void> _periksaNotifikasiColdStart() async {
    final RemoteMessage? pesanAwal = await _messaging.getInitialMessage();
    if (pesanAwal != null) {
      debugPrint('[FCM Cold Start Tap] Aplikasi dinyalakan dari notifikasi.');
      // Berikan jeda kecil agar struktur router dan element tree terpasang sempurna
      await Future.delayed(const Duration(milliseconds: 800));
      _arahkanRuteHalaman(pesanAwal.data);
    }
  }

  static Future<void> _tampilkanNotifikasiLokal(RemoteMessage message) async {
    final RemoteNotification? notification = message.notification;
    if (notification == null) return;

    await _localNotifications.show(
      notification.hashCode,
      notification.title,
      notification.body,
      NotificationDetails(
        android: AndroidNotificationDetails(
          _channelPenting.id,
          _channelPenting.name,
          channelDescription: _channelPenting.description,
          importance: Importance.max,
          priority: Priority.high,
          icon: '@mipmap/ic_launcher',
        ),
        iOS: const DarwinNotificationDetails(
          presentAlert: true,
          presentBadge: true,
          presentSound: true,
        ),
      ),
      payload: jsonEncode(message.data), // Sertakan data payload JSON sebagai string
    );
  }

  static void _onTapNotificationLokal(NotificationResponse response) {
    final String? payload = response.payload;
    if (payload != null && payload.isNotEmpty) {
      final Map<String, dynamic> data = jsonDecode(payload) as Map<String, dynamic>;
      _arahkanRuteHalaman(data);
    }
  }

  static void _arahkanRuteHalaman(Map<String, dynamic> data) {
    final String? tipeHalaman = data['type'] as String?;
    final String? idKonten = data['id'] as String?;

    if (tipeHalaman == null) return;

    // Alihkan rute menggunakan router aplikasi kita
    switch (tipeHalaman) {
      case 'promo':
        AppRouter.router.push('/promo');
        break;
      case 'produk':
        if (idKonten != null) AppRouter.router.push('/produk/$idKonten');
        break;
      case 'transaksi':
        if (idKonten != null) AppRouter.router.push('/transaksi/$idKonten');
        break;
      default:
        AppRouter.router.go('/');
    }
  }

  static Future<void> _kirimTokenKeServer(String token) async {
    // Logika pengiriman token ke backend kita agar server tahu alamat perangkat ini
    debugPrint('Mengirim token FCM ke database server backend...');
  }
}

Deep linking memungkinkan sistem operasi menangkap tautan eksternal (seperti klik link dari browser, SMS, atau email) dan mengalihkannya langsung untuk membuka aplikasi kita pada halaman yang sesuai. Terdapat dua jenis deep link:

  1. Custom URL Scheme (e.g., skema-kita://produk/123):
    • Kelebihan: Sangat mudah dikonfigurasi di file native.
    • Kekurangan: Tidak memiliki sistem verifikasi kepemilikan. Jika aplikasi lain menggunakan skema nama yang sama, sistem operasi akan memunculkan dialog pemilih aplikasi, atau tautan kita dapat dibajak oleh aplikasi lain.
  2. App Links (Android) & Universal Links (iOS) (e.g., https://domainkita.com/produk/123):
    • Kelebihan: Menggunakan protokol keamanan HTTPS yang diverifikasi langsung terhadap nama domain web resmi kita. Tidak dapat dibajak karena membutuhkan berkas sertifikat validasi di sisi server web kita. Jika aplikasi belum terinstal, tautan akan dibuka secara mulus di browser web biasa (graceful fallback).
    • Kekurangan: Membutuhkan konfigurasi server web dan kepemilikan nama domain.

Ubah berkas android/app/src/main/AndroidManifest.xml di dalam tag <activity> utama kita:

<activity
    android:name=".MainActivity"
    ...>
    
    <!-- 1. Konfigurasi Custom URL Scheme (skema-kita://) -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="skema-kita" />
    </intent-filter>

    <!-- 2. Konfigurasi Android App Links (HTTPS Domain) -->
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        
        <!-- Masukkan domain web resmi kita -->
        <data android:scheme="https" android:host="unisbadri.com" />
        <data android:scheme="http" android:host="unisbadri.com" />
    </intent-filter>
</activity>

Atribut android:autoVerify="true" memberi tahu Android untuk memverifikasi tautan domain HTTPS kita secara otomatis ke server web kita sesaat setelah aplikasi dipasang oleh pengguna.

  • Custom URL Scheme: Buka Xcode, masuk ke Info tab $\rightarrow$ URL Types. Tambahkan item baru dengan Identifier unik dan isi bagian URL Schemes dengan teks skema-kita.
  • Universal Links: Buka Xcode, masuk ke Signing & Capabilities tab $\rightarrow$ klik + Capability $\rightarrow$ pilih Associated Domains. Masukkan domain kita dengan format awalan applinks::
    applinks:unisbadri.com
    

Konfigurasi Server Web untuk Validasi Domain #

Agar sistem operasi Android dan iOS percaya bahwa aplikasi kita adalah pemilik sah dari domain unisbadri.com, kita wajib mengunggah berkas konfigurasi tanda tangan digital (digital signature) di server web kita. Berkas ini harus dapat diakses secara publik menggunakan protokol HTTPS tanpa pengalihan (redirect) pada jalur folder khusus .well-known/.

Buat file teks JSON bernama assetlinks.json dan letakkan di: https://unisbadri.com/.well-known/assetlinks.json

[
  {
    "relation": [
      "delegate_permission/common.handle_all_urls"
    ],
    "target": {
      "namespace": "android_app",
      "package_name": "com.unisbadri.app",
      "sha256_cert_fingerprints": [
        "14:6D:E9:83:C5:E0:14:10:BC:82:13:90:85:12:20:AA:BB:CC:DD:EE:FF:GG:HH:II:JJ:KK:LL:MM:NN"
      ]
    }
  }
]

[!NOTE] Nilai sha256_cert_fingerprints adalah sidik jari SHA-256 dari sertifikat penandatanganan aplikasi rilis kita (release signing certificate). Kita bisa mendapatkan nilai ini dari Google Play Console pada menu App Integrity atau mengekstraknya secara manual dari keystore rilis menggunakan perintah keytool.

Buat file teks JSON tanpa ekstensi berkas bernama apple-app-site-association dan letakkan di: https://unisbadri.com/.well-known/apple-app-site-association

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "AB12345XYZ.com.unisbadri.app",
        "paths": [
          "/produk/*",
          "/promo",
          "/transaksi/*"
        ]
      }
    ]
  }
}

Format nilai appID adalah penggabungan antara Apple Developer Team ID (terdiri dari 10 karakter alfanumerik) dengan iOS App Bundle Identifier kita, dipisahkan oleh tanda titik. Properti paths digunakan untuk membatasi pola URL mana saja yang diizinkan untuk membuka aplikasi kita secara langsung.


Untuk menangkap dan mengalirkan tautan deep link secara konsisten melintasi berbagai kondisi status aplikasi di Flutter, kita menggunakan package bantuan app_links. Package ini akan dikombinasikan dengan pustaka routing GoRouter untuk menangani navigasi deklaratif secara mulus.

1. Instalasi Dependensi #

dependencies:
  go_router: ^14.2.0
  app_links: ^6.1.1
// lib/core/router/app_router.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:app_links/app_links.dart';

class AppRouter {
  static final AppLinks _appLinks = AppLinks();
  
  static final GoRouter router = GoRouter(
    initialLocation: '/',
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => const Scaffold(
          body: Center(child: Text('Halaman Utama')),
        ),
      ),
      GoRoute(
        path: '/promo',
        builder: (context, state) => const Scaffold(
          body: Center(child: Text('Halaman Promo Spesial')),
        ),
      ),
      GoRoute(
        path: '/produk/:id',
        builder: (context, state) {
          final String id = state.pathParameters['id'] ?? '';
          return Scaffold(
            appBar: AppBar(title: const Text('Detail Produk')),
            body: Center(child: Text('Menampilkan Produk ID: $id')),
          );
        },
      ),
      GoRoute(
        path: '/transaksi/:id',
        builder: (context, state) {
          final String id = state.pathParameters['id'] ?? '';
          return Scaffold(
            appBar: AppBar(title: const Text('Detail Transaksi')),
            body: Center(child: Text('Menampilkan Detail Transaksi ID: $id')),
          );
        },
      ),
    ],
  );

  /// Inisialisasi listener deep link untuk memantau tautan masuk
  static Future<void> inisialisasiDeepLinking() async {
    // A. KONDISI COLD START (Aplikasi Mati Lalu Dibuka dari Link)
    try {
      final Uri? tautanAwal = await _appLinks.getInitialLink();
      if (tautanAwal != null) {
        debugPrint('[DeepLink Cold Start] Tautan awal terdeteksi: $tautanAwal');
        _prosesNavigasiDeepLink(tautanAwal);
      }
    } catch (e) {
      debugPrint('Gagal membaca tautan awal deep link: $e');
    }

    // B. KONDISI RUNTIME (Aplikasi Aktif di Foreground / Background)
    _appLinks.uriLinkStream.listen((Uri uri) {
      debugPrint('[DeepLink Runtime] Tautan masuk: $uri');
      _prosesNavigasiDeepLink(uri);
    }, onError: (err) {
      debugPrint('Error pada aliran data deep link: $err');
    });
  }

  static void _prosesNavigasiDeepLink(Uri uri) {
    // Kita mengambil jalur path dan query parameters secara presisi
    final String path = uri.path;
    
    if (path.isNotEmpty && path != '/') {
      // Mengalihkan fokus halaman router ke jalur tautan deep link
      router.push(path);
    }
  }
}

Hubungkan inisialisasi ini pada berkas main.dart kita:

// main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'core/notifications/notification_service.dart';
import 'core/router/app_router.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 1. Inisialisasi Firebase & Notifikasi
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  await NotificationService.inisialisasi();
  
  // 2. Inisialisasi Deep Linking Listener
  await AppRouter.inisialisasiDeepLinking();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Aplikasi Integrasi',
      routerConfig: AppRouter.router,
    );
  }
}

Desain Struktur Payload Pesan FCM dari Backend #

Untuk memastikan notifikasi push kita dapat memicu deep link navigasi secara otomatis ketika diketuk, server backend kita harus mengirimkan format data payload JSON yang terstruktur dengan memisahkan blok visual notification dengan blok logika data.

Berikut adalah skema payload JSON standar produksi untuk pengiriman melalui HTTP v1 API FCM:

{
  "message": {
    "token": "TOKEN_FCM_PERANGKAT_TARGET",
    "notification": {
      "title": "Promo Terbatas! 🚀",
      "body": "Dapatkan diskon 50% untuk produk buku Flutter terbaru."
    },
    "data": {
      "type": "produk",
      "id": "flutter-book-advanced",
      "click_action": "FLUTTER_NOTIFICATION_CLICK"
    },
    "android": {
      "notification": {
        "channel_id": "channel_penting_id",
        "click_action": "FLUTTER_NOTIFICATION_CLICK"
      }
    },
    "apns": {
      "payload": {
        "aps": {
          "sound": "default",
          "badge": 1
        }
      }
    }
  }
}

Properti click_action dengan nilai FLUTTER_NOTIFICATION_CLICK sangat penting untuk sistem Android lama agar sistem operasi mengetahui bahwa ketukan pada banner notifikasi harus diserahkan untuk memicu intent aplikasi kita, bukan sekadar membuang notifikasi tersebut.


Pengujian dan Praktik Terbaik Keamanan Notifikasi #

Setelah menyelesaikan seluruh langkah implementasi, kita wajib melakukan pengujian secara menyeluruh.

Untuk menguji apakah sistem operasi perangkat fisik atau emulator kita sudah mengenali tautan rute aplikasi kita dengan benar tanpa harus membuat link HTML manual, jalankan perintah terminal berikut:

  • Pengujian pada Android (via adb):
    # Menguji Custom URL Scheme
    adb shell am start -W -a android.intent.action.VIEW -d "skema-kita://produk/buku-flutter-123" com.unisbadri.app
    
    # Menguji App Links HTTPS Domain
    adb shell am start -W -a android.intent.action.VIEW -d "https://unisbadri.com/promo" com.unisbadri.app
    
  • Pengujian pada iOS Simulator:
    # Menguji Custom URL Scheme pada simulator iOS yang sedang aktif
    xcrun simctl openurl booted "skema-kita://transaksi/tr-9999"
    
    # Menguji Universal Links HTTPS Domain
    xcrun simctl openurl booted "https://unisbadri.com/produk/buku-flutter-123"
    

2. Praktik Terbaik Keamanan Payload Notifikasi #

  • Hindari Mengirim Data Sensifit: Jangan pernah menyertakan data sensitif pengguna (seperti kata sandi, alamat email, atau detail mutasi rekening) di dalam payload notifikasi push. Pihak ketiga atau sistem log sistem operasi dapat mengintip payload tersebut.
  • Gunakan Pola “Ping to Pull”: Dibandingkan mengirimkan data transaksi lengkap di dalam payload notifikasi untuk ditampilkan, kirimkan saja ID transaksi di dalam payload data, lalu biarkan aplikasi Flutter kita melakukan pemanggilan API HTTPS aman untuk menarik data terbaru dari server saat notifikasi dibuka.

Ringkasan #

  • Tiga State Aplikasi: Navigasi notifikasi wajib ditangani pada tiga kondisi status daur hidup aplikasi: foreground (manual via local notification), background (menggunakan onMessageOpenedApp), dan terminated (menggunakan getInitialMessage()).
  • Anotasi Entry-Point: Anotasikan fungsi handler latar belakang FCM dengan @pragma('vm:entry-point') untuk melindunginya dari pembuangan kode otomatis oleh compiler rilis Dart.
  • Validasi Domain: App Links dan Universal Links jauh lebih aman dibandingkan Custom URL Scheme karena memvalidasi domain HTTPS kita secara sah melalui file assetlinks.json dan apple-app-site-association di server web.
  • Routing Terpadu: Padukan package app_links dengan GoRouter untuk memusatkan penangkapan tautan URL dari luar dan mengalihkan fokus halaman secara instan menggunakan .push() deklaratif.
  • Verifikasi Fisik: Selalu lakukan pengujian notifikasi push remote menggunakan perangkat fisik rilis karena emulator sering kali tidak mensimulasikan perilaku sistem APNs/FCM latar belakang secara akurat.

← Sebelumnya: Background Tasks   Berikutnya: Internationalization →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact