Chương 8: Dịch vụ Nền tảng & Dịch vụ đặc thù MGW
Nắm vững hai nhóm dịch vụ chuyên sâu trong SDK: Nhóm dịch vụ nền tảng dùng chung (App Tracking Transparency trên iOS, kiểm tra cập nhật In-App Update, xin đánh giá In-App Review, theo dõi phân bổ Apple Search Ads) và Nhóm dịch vụ đặc thù chỉ có trên Provider MGW (Firebase Auth, Realtime Database, Business Intelligence, Local Notifications), cùng mẫu code bảo vệ đa nền tảng #if !TB_SDK_SAYKIT.
1. Tổng quan & Phân cấp Dịch vụ
Trong kiến trúc của MGW SDK, các dịch vụ nền tảng được tổ chức thành 2 nhóm rõ rệt dựa trên phạm vi hoạt động của SDK Provider:
| Nhóm dịch vụ | Danh sách dịch vụ | Phạm vi khả dụng | Cách truy xuất |
|---|---|---|---|
| Dịch vụ nền tảng dùng chung (Core Platform) |
AttService, AppUpdateService, IReviewService, SearchAdsService
|
Luôn khả dụng trên cả 2 Provider (MGW và SayKit) | Qua App.Review hoặc ServiceLocator.Get<T>() |
| Dịch vụ đặc thù MGW (MGW-Only) |
FirebaseService, RealtimeDbService, BIService, NotificationService
|
Chỉ khả dụng khi dùng Provider MGW (mặc định). Không tồn tại khi bật cờ TB_SDK_SAYKIT. |
Bắt buộc bọc trong #if !TB_SDK_SAYKIT và gọi ServiceLocator.TryGet<T>() |
Các dòng log mức Info (CoreLog.Info) trong chương này, ví dụ [AppUpdate] UpdateAvailable=..., [RTDB] Ready at ..., [Review] RequestReview (no-op in Editor), chỉ in ra khi CoreConfig.verboseLog bật hoặc là development build. Log mức Warn và Error (ví dụ [AppUpdate] Check failed, [Firebase] App creation failed) luôn in ra, kể cả bản release.
2. Cấp phép theo dõi iOS: AttService
Kể từ phiên bản iOS 14.5, Apple bắt buộc mọi ứng dụng muốn đọc mã định danh quảng cáo (IDFA) phải xin phép người dùng thông qua khung App Tracking Transparency (ATT). Dịch vụ TapBrain.Core.Services.AttService quản lý trạng thái và luồng xin cấp phép này.
Bảng API AttService
| Thành viên | Chữ ký | Hành vi Runtime |
|---|---|---|
Status |
string Status { get; } |
Trả về tên chuỗi trạng thái từ ATTrackingStatusBinding.GetAuthorizationTrackingStatus() trên iOS (ví dụ: "AUTHORIZED", "DENIED", "NOT_DETERMINED", "RESTRICTED"). Khi build target không phải iOS (Android, Standalone), luôn trả về "NOT_SUPPORTED". Lưu ý: mã dùng #if UNITY_IOS (không loại trừ Editor), nên trong Editor kết quả phụ thuộc vào build target đang chọn. |
IsAuthorized |
bool IsAuthorized { get; } |
Trả về true nếu người dùng đã bấm chấp nhận theo dõi trên iOS. Khi build target không phải iOS, luôn trả về false. |
RequestIfNeeded() |
IEnumerator RequestIfNeeded() |
Nếu trạng thái đang là NOT_DETERMINED trên thiết bị iOS thật, tiến hành lưu cờ core.att_requested và hiển thị hộp thoại xin quyền của hệ điều hành. |
OpenAppSettings() |
void OpenAppSettings() |
Mở thẳng trang Cài đặt ứng dụng trên iOS (thông qua native plugin RequestATTracking.m) để người dùng có thể chủ động bật lại quyền theo dõi nếu trước đó lỡ bấm từ chối. |
- Điều kiện dừng: Coroutine RequestIfNeeded() lặp chờ cho đến khi trạng thái rời khỏi NOT_DETERMINED (người chơi đã bấm Cho phép hoặc Từ chối), hoặc kết thúc ngay nếu không chạy trên thiết bị iOS thật / đã xác định quyền trước đó. Pipeline khởi động khống chế thời gian chờ tối đa là 120 giây.
- Khác biệt MGW vs SayKit: Trên Provider MGW, Bootstrapper tự động đưa bước ATT vào pipeline khởi động trước khi khởi tạo AppLovin MAX. Ngược lại, khi chuyển sang Provider SayKit (vốn có luồng đồng thuận người dùng và ATT riêng), cờ provider.HandlesConsent == true khiến Bootstrapper tự động bỏ qua bước ATT của Core để tránh hỏi người dùng hai lần.
3. Kiểm tra Cập nhật: AppUpdateService
Lớp TapBrain.Core.Services.AppUpdateService kiểm tra xem trên Store đã có phiên bản game mới hơn hay chưa mà không can thiệp vào giao diện: Dịch vụ chỉ trả về kết quả, việc hiển thị popup và trải nghiệm người dùng hoàn toàn do mã game quyết định (Result only: the game decides UI).
Bảng API AppUpdateService
| Thành viên | Chữ ký | Mô tả chi tiết |
|---|---|---|
UpdateAvailable |
bool UpdateAvailable { get; } |
Cờ cho biết hiện tại có bản cập nhật mới trên Store hay không. Mặc định false cho tới khi Check() chạy xong. |
Checked |
event Action<bool> Checked; |
Sự kiện phát ra khi quá trình kiểm tra kết thúc, truyền vào giá trị của UpdateAvailable. Chỉ phát một lần mỗi lần chạy Check() (mặc định chạy một lần ở cuối boot), và không bao giờ phát trong Editor hay nền tảng khác Android/iOS. Code của scene đăng ký muộn phải đọc UpdateAvailable trước rồi mới đăng ký Checked. |
Check() |
IEnumerator Check() |
Thực hiện kiểm tra phiên bản mới. Trên Android: sử dụng Google Play In-App Update API (AppUpdateManager). Trên iOS: gửi truy vấn đến iTunes Lookup API (https://itunes.apple.com/lookup?bundleId=...). |
OpenStorePage |
void OpenStorePage(CoreConfig config) |
Mở trang tải game trên Store. iOS mở URL itms-apps://itunes.apple.com/app/id... theo config.iosAppStoreId; Android mở market://details?id=... với Application.identifier (không đọc CoreConfig.androidPackageName). |
CompareVersions |
static int CompareVersions(string a, string b) |
Hàm tiện ích tĩnh so sánh 2 chuỗi phiên bản (ví dụ "1.2.0" và "1.2.1"). Trả về -1 nếu a < b, 0 nếu bằng nhau, 1 nếu a > b. Các phân đoạn thiếu được coi là 0 (ví dụ: "1.2" tương đương "1.2.0"). |
Trong CoreConfig, trường enableAppUpdateCheck (mặc định bật) cho phép hệ thống tự động chạy ngầm coroutine Check() trong pha Background của Bootstrapper.
- Điều kiện dừng của Check(): Coroutine kết thúc ngay sau đúng một vòng gửi nhận mạng (network round-trip).
- Log Console khi hoàn tất: [AppUpdate] UpdateAvailable=True hoặc [AppUpdate] UpdateAvailable=False. Đây là log mức Info: chỉ hiện khi CoreConfig.verboseLog bật hoặc là development build; Warn/Error thì luôn hiện (xem ghi chú về log ở mục 1). Trong Editor không có log này và Checked không được phát.
- Log cảnh báo nếu lỗi trên Android: [AppUpdate] Check failed: <lỗi> (Warn, luôn hiện). Trên iOS, lỗi mạng khi tra cứu iTunes không có log; Checked(false) vẫn được phát.
4. Đánh giá ứng dụng: App.Review & IReviewService
Dịch vụ đánh giá ứng dụng được trừu tượng hóa qua interface TapBrain.Core.Services.IReviewService và bạn có thể kích hoạt từ bất kỳ đâu qua App.Review.RequestReview().
Cơ chế hoạt động & Hợp đồng nguyên tắc
- Trên Android: Gọi In-App Review Flow của Google Play (
Google.Play.Review.ReviewManager). Nếu bước yêu cầu review flow (RequestReviewFlow) báo lỗi, SDK mở trang Store bằng URLmarket://details?id=...(vớiApplication.identifier). Nếu lỗi xảy ra ở bước hiển thị (LaunchReviewFlow) thì không có fallback. - Trên iOS: Gọi native function
requestReview()(thông qua StoreKit trong fileiOSReviewRequest.m). - Trong Unity Editor và nền tảng khác: Không làm gì, chỉ in log Info
[Review] RequestReview (no-op in Editor)(xem quy ước log ở mục 1). - Khác biệt trên SayKit: Khi chuyển sang Provider SayKit, dịch vụ này được thay thế bằng
SayKitReviewServicetrỏ về popup đánh giá riêng của SayKit (SayKit.showRateAppPopup()).
Cả Apple và Google đều áp đặt cơ chế giới hạn tần suất (rate-limiting) rất nghiêm ngặt đối với hộp thoại In-App Review (hộp thoại có thể không hiện dù bạn đã gọi RequestReview()). Hơn nữa, API của hệ điều hành hoàn toàn không báo về việc người chơi đã đánh giá mấy sao hay đã đóng hộp thoại. Do đó, bạn không bao giờ được phép chặn luồng game hoặc hứa hẹn trao thưởng vàng/kim cương dựa trên việc bấm nút Đánh giá!
5. Phân bổ Apple Search Ads: SearchAdsService
Dịch vụ TapBrain.Core.Services.SearchAdsService lấy mã token phân bổ thông qua framework Apple AdServices trên iOS 14.3+ (file native SearchAdsAttribution.m), giải mã dữ liệu chiến dịch và tự động bắn sự kiện phân tích.
Bảng API SearchAdsService
| Thành viên | Chữ ký | Mô tả hoạt động |
|---|---|---|
Cached |
Payload Cached { get; } |
Đọc dữ liệu phân bổ đã được giải mã từ khóa core.asa_payload trong LocalStorage. Trả về null nếu chưa có dữ liệu. |
FetchOnce() |
void FetchOnce() |
Chỉ chạy trên iOS thiết bị thật (Editor và Android không làm gì). Bỏ qua nếu đã có payload trong cache; nếu cả 3 lần gọi đều lỗi thì không có payload và sẽ thử lại ở lần mở game sau. Chạy dạng fire-and-forget, không làm nghẽn tiến trình boot của game. |
Lớp dữ liệu SearchAdsService.Payload chứa các trường thông tin chiến dịch:
// Nested type: SearchAdsService.Payload ([Serializable], parsed with JsonUtility)
public sealed class Payload
{
public bool attribution; // true when the install came from a Search Ads click
public string orgId; // advertiser organization id
public string campaignId; // campaign id
public string adGroupId; // ad group id
public string keywordId; // keyword id
public string adId; // ad id
public string countryOrRegion; // country or region
public string conversionType; // Download or Redownload
public string clickDate; // click timestamp
}
- Điều kiện dừng của FetchRoutine: Coroutine kết thúc ngay khi nhận phản hồi HTTP thành công, hoặc sau tối đa 3 lần gọi (MaxAttempts = 3, tức 2 lần thử lại), chờ 5 giây thực trước mỗi lần gọi (kể cả lần đầu). Nếu không lấy được token thì dừng ngay.
- Tự động gửi Analytics: Khi nhận được kết quả hợp lệ với attribution == true, SDK tự động gửi sự kiện asa_attribution kèm các thông số chiến dịch sang IAnalyticsService.
- Bỏ qua Sandbox: Nếu orgId == "1234567890" (phản hồi giả lập của Apple trong môi trường kiểm thử), SDK lưu bộ nhớ đệm nhưng bỏ qua không gửi sự kiện phân tích để tránh làm bẩn số liệu.
6. Các Dịch vụ Đặc thù MGW (MGW-Only)
Toàn bộ các dịch vụ dưới đây nằm trong thư mục Runtime/Sdk/MGW/** và thuộc assembly TapBrain.Sdk.MGW. Assembly này được cấu hình ràng buộc biên dịch loại trừ khi có cờ TB_SDK_SAYKIT:
Nếu bạn tham chiếu trực tiếp đến các lớp FirebaseService, RealtimeDbService, BIService hay NotificationService mà không bọc điều kiện biên dịch, dự án của bạn sẽ bị lỗi biên dịch ngay khi chuyển sang Provider SayKit. Bạn bắt buộc phải áp dụng mẫu chuẩn sau:
using TapBrain.Core;
#if !TB_SDK_SAYKIT
using TapBrain.Sdk.Mgw;
#endif
// Inside your own method:
#if !TB_SDK_SAYKIT
if (ServiceLocator.TryGet<RealtimeDbService>(out var rtdb) && rtdb.IsReady)
{
// Safe to use Firebase Realtime Database here
}
#endif
7. FirebaseService & Cơ chế Chốt chặn (Gating)
Lớp TapBrain.Sdk.Mgw.FirebaseService quản lý vòng đời và tính khả dụng của thư viện Firebase Unity.
Bảng API FirebaseService
| Thành viên | Chữ ký | Hành vi Runtime |
|---|---|---|
IsAvailable |
bool IsAvailable { get; } |
Trả về true khi các thư viện phụ thuộc của Firebase đã sẵn sàng VÀ instance FirebaseApp.DefaultInstance đã được tạo thành công. |
AuthUid |
string AuthUid { get; } |
Chuỗi User ID lấy từ Firebase Authentication (phiên đăng nhập hiện có hoặc đăng nhập ẩn danh mới ở boot step Auth). Là null khi Firebase không khả dụng, khi CoreConfig.enableFirebase hoặc enableAuth tắt, hoặc khi đăng nhập thất bại ([Firebase] Anonymous sign-in failed, Warn). |
SignInAnonymously() |
IEnumerator SignInAnonymously() |
Đăng nhập tài khoản ẩn danh (Anonymous Auth) để thỏa mãn quy tắc bảo mật của Realtime Database. Hoạt động bất đồng bộ không gây nghẽn game. |
Cơ chế Gating: Log lỗi rõ ràng, không lỗi dây chuyền
Trong nhiều dự án Unity, việc thiếu file cấu hình google-services.json (Android) hoặc GoogleService-Info.plist (iOS) thường dẫn đến việc các dịch vụ con (Remote Config, Database, Analytics, Auth) liên tục văng exception làm đỏ rực Console.
FirebaseService áp dụng cơ chế Gating tập trung: Trong phương thức Initialize, nó kiểm tra dependencies và khởi tạo thử FirebaseApp.DefaultInstance trong khối try-catch. Nếu thiếu cấu hình, nó lập tức gán IsAvailable = false và in một dòng log lỗi (Error, luôn hiện) hướng dẫn nhà phát triển, thay vì để từng dịch vụ con tự ném exception:
[Firebase] App creation failed, Firebase disabled for this session. Add the per-game config (Assets/google-services.json, Assets/GoogleService-Info.plist; see Documentation~/KEYS.md in com.mgw.sdk): <exception message>
Thiếu thư viện phụ thuộc thì thay vào đó sẽ có dòng [Firebase] Dependencies unavailable: <status>. Khi CoreConfig.enableFirebase = false, bước Firebase bị bỏ qua: IsAvailable luôn false và không có log nào.
Tất cả các dịch vụ phụ thuộc phía sau (như RealtimeDbService, FirebaseRemoteConfigService) đều kiểm tra cờ _firebase.IsAvailable trước khi chạy, giúp phiên làm việc của game vẫn hoạt động trơn tru mà không bị crash dây chuyền.
8. Firebase Realtime Database: RealtimeDbService
Lớp TapBrain.Sdk.Mgw.RealtimeDbService cung cấp lối truy cập có phạm vi an toàn vào Firebase Realtime Database. Dữ liệu luôn được ghi vào cấu trúc đường dẫn phân nhánh theo môi trường:
/<realtimeDbRoot>/<DataSet>/...
realtimeDbRoot: Chuỗi định danh gốc cấu hình trongCoreConfig.asset(ví dụ: tên game của bạn). Nếu để trống hoặc giữ nguyên giá trị mặc định, SDK dùng một root chung cho mọi game, nên hãy đặt root riêng cho mỗi game.DataSet: Thuộc tính trả về"debug"nếu game đang chạy bản Debug (Debug.isDebugBuild == true), và trả về"release"đối với bản phát hành thực tế. Nhờ vậy, dữ liệu chơi thử của lập trình viên và QA không bao giờ làm sai lệch cơ sở dữ liệu thật của người dùng!
Bảng API RealtimeDbService
| Phương thức / Thuộc tính | Chữ ký | Mô tả hoạt động |
|---|---|---|
IsReady |
bool IsReady { get; } |
Trả về true nếu tham chiếu DatabaseReference gốc đã được tạo thành công. Chỉ đạt được khi CoreConfig.enableFirebase và enableRealtimeDb bật, FirebaseService.IsAvailable là true và boot step RealtimeDb+BI đã chạy. |
DataSet |
string DataSet { get; } |
Trả về "debug" hoặc "release" tùy theo cờ build. |
Child |
DatabaseReference Child(string path) |
Lấy tham chiếu trực tiếp đến node con path bên dưới nhánh dataset hiện tại (đã nằm trong /<realtimeDbRoot>/<DataSet>). Trả về null khi chưa IsReady, nên cần kiểm tra trước khi dùng. |
SetJson |
void SetJson(string path, string json, Action<bool> done = null) |
Ghi chuỗi JSON lên đường dẫn path. Callback done nhận về true nếu ghi thành công, hoặc false nếu thất bại. Khi chưa IsReady, done(false) được gọi đồng bộ ngay lập tức. |
GetJson |
void GetJson(string path, Action<string> done) |
Đọc chuỗi JSON từ đường dẫn path. Callback done nhận về chuỗi JSON, hoặc null nếu node không tồn tại, có lỗi, hoặc chưa IsReady (khi đó gọi đồng bộ ngay lập tức). |
Khi khởi tạo thành công, Console sẽ in dòng log xác nhận (mức Info, chỉ hiện khi Verbose bật, xem ghi chú ở mục 1):
[RTDB] Ready at /<realtimeDbRoot>/<DataSet>.
9. Hồ sơ Người dùng: BIService
Lớp TapBrain.Sdk.Mgw.BIService phụ trách thu thập và đẩy hồ sơ người dùng (User Profile) lên Firebase Realtime Database tại đường dẫn:
/<realtimeDbRoot>/<DataSet>/user_profile/<UserId>/core
Phương thức chính:
public void PushUserProfile(string attStatus)
SDK tự gọi PushUserProfile(AttService.Status) đúng một lần ở boot step RealtimeDb+BI, và chỉ khi CoreConfig.enableRealtimeDb và enableBI đều bật và RealtimeDbService.IsReady; game không cần gọi lại.
Hồ sơ này chứa 14 trường thông số độc lập với game: userId, platform, appVersion, firstVersion, installTimeUtc, lastSeenUtc, sessionCount, country, deviceModel, ramMb, removeAds, iapCount, adRevenueUsd, và attStatus.
Phía dự án game có thể thoải mái mở rộng thêm thông tin người chơi bằng cách tự ghi vào các node con kế bên, ví dụ: user_profile/<UserId>/game (không dùng tên core) qua RealtimeDbService.SetJson.
10. Thông báo Cục bộ: NotificationService
Lớp TapBrain.Sdk.Mgw.NotificationService quản lý việc lên lịch thông báo cục bộ (Local Notifications) bằng gói com.unity.mobile.notifications mà không cần máy chủ gửi tin (No server push).
Hộp thoại xin quyền thông báo của hệ điều hành (iOS và Android 13+) hiện ở boot step cuối Notifications (chạy sau IAP, chỉ khi CoreConfig.enableNotifications bật). Service cũng đăng ký kênh Android default. Trước khi bước này xong, và trong Editor, IsPermissionGranted là false nên Schedule bị bỏ qua.
Bảng API NotificationService
| Thành viên | Chữ ký | Mô tả chi tiết |
|---|---|---|
IsPermissionGranted |
bool IsPermissionGranted { get; } |
Cho biết người dùng đã cấp quyền hiển thị thông báo hay chưa (hỗ trợ kiểm tra quyền trên Android 13+ và iOS). |
ScheduleRequested |
event Action ScheduleRequested; |
Sự kiện được kích hoạt mỗi khi ứng dụng chuyển sang trạng thái tạm dừng (Pause). Đây là lúc game đăng ký các thông báo nhắc nhở người chơi quay lại. |
Schedule |
void Schedule(string title, string body, DateTime fireTimeLocal, string data = "") |
Lên lịch hiển thị một thông báo tại thời điểm fireTimeLocal (giờ máy). data là chuỗi tùy ý đính kèm thông báo (IntentData trên Android, Data trên iOS). Nếu thời gian truyền vào đã trôi qua hoặc chưa có quyền, lệnh sẽ bị bỏ qua âm thầm. |
CancelAll |
void CancelAll() |
Hủy toàn bộ thông báo đang chờ. Trên iOS còn xóa các thông báo đã hiển thị và đặt App Badge về 0; trên Android chỉ hủy thông báo đã lên lịch. |
Quy ước vòng đời thông báo & Cấu hình Icon trên Android
- Quy ước tự động dọn dẹp: Khi người chơi mở lại game (Resume),
NotificationServicetự động gọiCancelAll()để dọn dẹp khay thông báo. Khi người chơi thoát hoặc ẩn game (Pause), service gọiCancelAll()rồi phát sự kiệnScheduleRequestedđể game lên lịch mới. Nhờ quy ước này, thông báo được lên lịch lúc Pause sẽ bị hủy khi người chơi quay lại game. Hãy chỉ lên lịch trong handler củaScheduleRequested; thông báo lên lịch giữa phiên chơi với thời gian ngắn có thể bắn khi game đang mở. - Cấu hình Icon bắt buộc trên Android: Trong Unity Editor, bạn phải vào
Project Settings > Mobile Notifications > Androidvà cấu hình 2 icon: icon nhỏ với Identifier là"icon_0"và icon lớn với Identifier là"icon_1". SDK luôn tham chiếu hai icon này khi lên lịch thông báo Android, nên hãy cấu hình đủ cả hai.
11. Ví dụ C# Thực tế Hoàn chỉnh
Ví dụ 1: Xây dựng trình quản lý cập nhật game (UpdateChecker)
Đoạn mã sau đọc kết quả từ AppUpdateService (đọc cờ trước, đăng ký Checked sau, và hủy đăng ký khi Dispose) rồi hiển thị thông báo nếu có phiên bản mới. Trong Editor Check() không chạy nên sẽ không có thông báo:
using System;
using TapBrain.Core;
using TapBrain.Core.Services;
namespace MyGame.Services
{
// Create in a scene's Start(), call Dispose() in OnDestroy().
public sealed class GameUpdateCoordinator : IDisposable
{
private readonly AppUpdateService _updateService;
private readonly CoreConfig _config;
public GameUpdateCoordinator()
{
_updateService = ServiceLocator.Get<AppUpdateService>();
_config = ServiceLocator.Get<CoreConfig>();
// Checked fires once per Check() (by default once at the end of boot), so it may already
// have fired before this scene started: read the flag first, subscribe only if still unknown.
if (_updateService.UpdateAvailable)
{
OnUpdateChecked(true);
}
else
{
_updateService.Checked += OnUpdateChecked;
}
}
public void Dispose()
{
_updateService.Checked -= OnUpdateChecked;
}
private void OnUpdateChecked(bool hasNewVersion)
{
if (!hasNewVersion) return;
// Replace with your own update popup if you need a real call to action.
App.Overlay.Toast("Đã có phiên bản mới trên Store! Vui lòng cập nhật.", 3f);
}
// Wire this to your own "Update" button.
public void OpenStore()
{
_updateService.OpenStorePage(_config);
}
}
}
Ví dụ 2: Lớp đồng bộ điểm và hẹn giờ thông báo an toàn đa Provider
Ví dụ hoàn chỉnh minh họa cách sử dụng đồng thời RealtimeDbService và NotificationService với điều kiện biên dịch #if !TB_SDK_SAYKIT:
using System;
using TapBrain.Core;
using UnityEngine;
#if !TB_SDK_SAYKIT
using TapBrain.Sdk.Mgw;
#endif
namespace MyGame.Services
{
// Create once (e.g. in a bootstrap scene's Start) and keep it for the whole session;
// call Dispose() when you drop it, otherwise the ScheduleRequested subscription leaks.
public sealed class PlayerSyncManager : IDisposable
{
#if !TB_SDK_SAYKIT
private NotificationService _notifications;
#endif
public PlayerSyncManager()
{
#if !TB_SDK_SAYKIT
// Schedule reminders when the app is paused (ScheduleRequested fires right after CancelAll).
if (ServiceLocator.TryGet<NotificationService>(out var notifs))
{
_notifications = notifs;
_notifications.ScheduleRequested += OnScheduleReminders;
}
#endif
}
public void SaveHighScore(int score)
{
#if !TB_SDK_SAYKIT
if (ServiceLocator.TryGet<RealtimeDbService>(out var db) && db.IsReady)
{
var userId = ServiceLocator.Get<UserIdentity>().UserId;
var json = $"{{\"highScore\":{score},\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}}}";
// The path is relative to /<realtimeDbRoot>/<DataSet>/.
db.SetJson($"scores/{userId}", json, success =>
{
CoreLog.Info("Sync", $"High score sync: {(success ? "ok" : "failed")}");
});
}
#else
CoreLog.Info("Sync", "Running on the SayKit provider, Firebase sync skipped.");
#endif
}
#if !TB_SDK_SAYKIT
private void OnScheduleReminders()
{
if (_notifications == null || !_notifications.IsPermissionGranted) return;
// Reminder 24 hours from now (local time).
_notifications.Schedule(
title: "Kho báu đã sẵn sàng!",
body: "Đăng nhập ngay hôm nay để nhận phần thưởng miễn phí của bạn.",
fireTimeLocal: DateTime.Now.AddHours(24),
data: "daily_reward"
);
}
#endif
public void Dispose()
{
#if !TB_SDK_SAYKIT
if (_notifications != null)
{
_notifications.ScheduleRequested -= OnScheduleReminders;
_notifications = null;
}
#endif
}
}
}
12. Các sai lầm thường gặp (So sánh Sai vs Đúng)
// 1. Referencing an MGW-only service without a define guard
using TapBrain.Sdk.Mgw;
var fb = ServiceLocator.Get<FirebaseService>();
// Does not compile once the SayKit provider (TB_SDK_SAYKIT) is selected
// 2. Granting a reward for pressing Review
App.Review.RequestReview();
GrantPlayerCoins(500); // the OS never reports the outcome; policy violation
// 3. Bypassing RealtimeDbService with the raw root reference
FirebaseDatabase.DefaultInstance.RootReference
.Child("user_data/score").SetValueAsync(100);
// Ignores /<realtimeDbRoot>/<DataSet>: test data lands next to production data
// 4. Scheduling a notification in the middle of a session
notifs.Schedule("Reminder", "...", DateTime.Now.AddMinutes(1));
// May fire while the player is still in the game
// 1. Always guard with #if !TB_SDK_SAYKIT
#if !TB_SDK_SAYKIT
if (ServiceLocator.TryGet<FirebaseService>(out var fb)
&& fb.IsAvailable)
{
// Safe to use
}
#endif
// 2. Call Review as a feedback feature only
App.Review.RequestReview();
// Never make a reward depend on it
// 3. Use RealtimeDbService: Child() and SetJson() are already scoped
// under /<realtimeDbRoot>/<DataSet> (debug or release)
db.SetJson("scores/123", json);
// 4. Schedule only inside ScheduleRequested, with a named handler you unsubscribe
notifs.ScheduleRequested += OnScheduleReminders; // e.g. in Start()
// ...
notifs.ScheduleRequested -= OnScheduleReminders; // e.g. in OnDestroy()
13. Checklist kiểm tra nhanh
FirebaseService, RealtimeDbService, BIService, NotificationService phải nằm trong #if !TB_SDK_SAYKIT.App.Review.RequestReview().Project Settings > Mobile Notifications cấu hình icon_0 và icon_1.iosAppStoreId đã được điền mã định danh thật để AppUpdateService.OpenStorePage mở đúng trang App Store trên iOS. Review không dùng trường này.