UNPKG

@bzbs/react-providers

Version:

A collection of React Context Provider for Buzzebees apps

1,534 lines (1,210 loc) 44.9 kB
# @bzbs/react-providers > **Version:** 3.2.4 | **License:** ISC | **Author:** Buzzebees Co., Ltd. > **Repository:** [Azure DevOps](https://buzzebees.visualstudio.com/Buzzebees/_git/React_Providers) A collection of Zustand stores and service utilities for Buzzebees loyalty/reward apps. This package is the **implementation layer** on top of [`@bzbs/react-api-client`](./react-api-client.md), wrapping API calls with React-friendly state, actions, and side-effects (analytics, token management, event broadcasting). > ℹ️ **Breaking change in 3.0.0:** The Context API layer (`/providers`, `./context` export, `DefaultMainAppProvider`) has been **removed**. Only the Zustand stores remain. If you are upgrading from `2.x`, migrate to the Zustand stores documented below. --- ## Table of Contents - [Installation](#installation) - [Architecture Overview](#architecture-overview) - [Quick Start](#quick-start) - [Package Exports](#package-exports) - [Zustand Stores Reference](#zustand-stores-reference) - [useBuzzebeesAppStore](#usebuzzebeesappstore) - [useAuthStore](#useauthstore) - [useUserStore](#useuserstore) - [useCartStore](#usecartstore) - [useLocaleStore](#uselocalestore) - [useAnalyticsStore](#useanalyticsstore) - [useAlertStore](#usealertstore) - [useConfirmStore](#useconfirmstore) - [usePopupStore](#usepopupstore) - [useLoadingIndicatorStore](#useloadingindicatorstore) - [useNotificationStore](#usenotificationstore) - [useMaintenanceStore](#usemaintenancestore) - [useConsentStore](#useconsentstore) - [useRegistrationStore](#useregistrationstore) - [useAddressStore](#useaddressstore) - [useZipCodeStore](#usezipcodestore) - [createCampaignsStore](#createcampaignsstore) - [createCampaignDetailStore](#createcampaigndetailstore) - [createCategoriesStore](#createcategoriesstore) - [createDashboardStore](#createdashboardstore) - [createPointLogStore](#createpointlogstore) - [createPurchaseStore](#createpurchasestore) - [useCouponStore](#usecouponstore) - [createFavoriteCampaignsStore](#createfavoritecampaignsstore) - [TokenFunctions Interface](#tokenfunctions-interface) - [Service Utilities](#service-utilities) - [addDefaultHeaderInterceptor](#adddefaultheaderinterceptor) - [addBlake2OtpSignatureInterceptor](#addblake2otpsignatureinterceptor) - [Utility Functions](#utility-functions) - [eventEmitter](#eventemitter) - [Analytics Utilities](#analytics-utilities) - [trackCommonEvents](#trackcommonevents) - [analytics (global object)](#analytics-global-object) - [useAnalytics Hook](#useanalytics-hook) - [Locale Utilities](#locale-utilities) - [Campaign Utilities](#campaign-utilities) - [Constants](#constants) - [campaignType](#campaigntype) - [campaignPointType](#campaignpointtype) - [campaignInterfaceType](#campaigninterfacetype) - [Types Reference](#types-reference) - [Relationship to @bzbs/react-api-client](#relationship-to-bzbsreact-api-client) - [Migration from 2.x](#migration-from-2x) - [Development Commands](#development-commands) --- ## Installation ```bash npm install @bzbs/react-providers ``` **Peer dependencies:** ```bash npm install react react-dom axios ``` **Also required** (provides `BzbsService`): ```bash npm install @bzbs/react-api-client ``` --- ## Architecture Overview ``` @bzbs/react-providers ├── Zustand Stores import from '@bzbs/react-providers/zustand' ├── Service Utilities HTTP interceptors, URL helpers, event emitter ├── Analytics Utils Matomo tracking wrappers ├── Locale Utils Locale LCID mapping ├── Campaign Utils isPointEnough, isCodeAutoUse └── Constants Campaign types, point types ``` **Dependencies flow:** ``` @bzbs/react-api-client (BzbsService) useBuzzebeesAppStore Central config store useAuthStore, useUserStore, useCartStore, ... Feature stores ``` All feature stores read `bzbsService` from `useBuzzebeesAppStore` to make API calls. You must call `useBuzzebeesAppStore.getState().configure(...)` (or use individual setters) before using any other store. --- ## Quick Start ```typescript import axios from 'axios'; import { BzbsService } from '@bzbs/react-api-client'; import { useBuzzebeesAppStore, addDefaultHeaderInterceptor, useAuthStore, } from '@bzbs/react-providers/zustand'; // 1. Create an Axios instance const axiosClient = axios.create({ headers: { 'Content-Type': 'application/json', 'App-Id': 'YOUR_APP_ID', 'Subscription-Key': 'YOUR_SUBSCRIPTION_KEY', }, }); // 2. Create the BzbsService instance const bzbsService = new BzbsService(axiosClient, 'https://api.buzzebees.com'); // 3. Define token persistence functions const tokenFunctions = { getToken: async () => localStorage.getItem('bzbs_token'), setToken: (token: string) => localStorage.setItem('bzbs_token', token), removeToken: () => localStorage.removeItem('bzbs_token'), }; // 4. Attach auth/header interceptors addDefaultHeaderInterceptor( axiosClient, 'YOUR_APP_ID', 'YOUR_SUBSCRIPTION_KEY', tokenFunctions, '1.0' ); // 5. Configure the central app store (call this once at app startup) useBuzzebeesAppStore.getState().configure({ appId: 'YOUR_APP_ID', appName: 'your-app', bzbsService, tokenFunctions, tokenType: 'jwt', urls: { webCallback: 'https://yourapp.com/callback', cart: 'https://cart.buzzebees.com', }, config: { defaultDashboardConfig: 'main', defaultDashboardMode: 'main', defaultMenuConfig: 'default', defaultCampaignConfig: 'default', supportPointUnits: [], // Configure point service units, e.g. ['POINT', 'COIN'] }, }); // 6. Use stores in components function LoginScreen() { const { isLoading, isLoggedIn, actions } = useAuthStore(); const handleLogin = async () => { const result = await actions.loginWithUsernamePassword('user@example.com', 'password'); if (result.type === 'success') { console.log('Logged in!'); } }; return <button onClick={handleLogin} disabled={isLoading}>Login</button>; } ``` --- ## Package Exports Only the `/zustand` entry point is published. | Import Path | Contents | |---|---| | `@bzbs/react-providers/zustand` | All Zustand stores, hooks, service utilities, analytics utilities, constants, and types | | `@bzbs/react-providers` | Alias of `/zustand` (`main`/`module` point to the same build) | --- ## Zustand Stores Reference All stores are available from `@bzbs/react-providers/zustand`. --- ### useBuzzebeesAppStore **Central configuration store.** Must be configured before any other store is used. ```typescript import { useBuzzebeesAppStore } from '@bzbs/react-providers/zustand'; ``` **State:** ```typescript type BuzzebeesAppState = { appId: string; appName: string; bzbsService: BzbsService | null; uuid: string; macAddress: string; clientVersion: string; os: string; platform: string; fcmToken: string; deviceNotificationEnabled: boolean; urls: { webCallback: string; cart: string }; tokenFunctions: TokenFunctions | null; tokenType: AuthTokenType; // 'jwt' | 'auth_token' config: { defaultDashboardConfig: string; defaultDashboardMode: 'main' | 'sub'; defaultMenuConfig: string; defaultCampaignConfig: string; supportPointUnits: string[]; // Point-service units supported by the app }; }; ``` **Actions:** ```typescript type BuzzebeesAppActions = { // Bulk configure call this once at app startup configure(config: { appId: string; appName: string; bzbsService: BzbsService; tokenFunctions: TokenFunctions; tokenType?: AuthTokenType; uuid?: string; macAddress?: string; clientVersion?: string; os?: string; platform?: string; fcmToken?: string; deviceNotificationEnabled?: boolean; urls: { webCallback: string; cart: string }; config?: { defaultDashboardConfig?: string; defaultDashboardMode?: 'main' | 'sub'; defaultMenuConfig?: string; defaultCampaignConfig?: string; supportPointUnits?: string[]; }; }): void; // Individual setters setAppId(appId: string): void; setAppName(appName: string): void; setBzbsService(service: BzbsService): void; setUuid(uuid: string): void; setMacAddress(macAddress: string): void; setClientVersion(clientVersion: string): void; setOs(os: string): void; setPlatform(platform: string): void; setFcmToken(fcmToken: string): void; setDeviceNotificationEnabled(enabled: boolean): void; setUrls(urls: { webCallback: string; cart: string }): void; setTokenType(tokenType: AuthTokenType): void; setTokenFunctions(tokenFunctions: TokenFunctions): void; }; ``` > **`supportPointUnits`** When non-empty, the campaign detail store routes redemption point checks through the point-service balances on the user (`pointServiceBalance`) rather than the regular point balance. Leave as `[]` if your app does not use the point-service feature. **Usage:** ```typescript // Access state in a component const { appId, bzbsService } = useBuzzebeesAppStore(); // Access state outside React (e.g., service files) const state = useBuzzebeesAppStore.getState(); ``` --- ### useAuthStore Manages the full authentication lifecycle: login, logout, OTP, token validation, and third-party providers (Google, Facebook, Apple, Line). ```typescript import { useAuthStore } from '@bzbs/react-providers/zustand'; ``` **State:** ```typescript type AuthState = { isInitialized: boolean; versionData: Version | null; rawVersionData: unknown | null; data: LoginResponse | ResumeResponse | null; thirdPartyLoginData?: ThirdPartyLoginData; // Set when the most recent login was third-party isLoading: boolean; isLoadingToken: boolean; error: ErrorResponse | null; token: string | null; isLoggedIn: boolean; }; type ThirdPartyLoginData = | { type: 'google'; idToken: string } | { type: 'facebook'; accessToken: string } | { type: 'apple'; idToken: string; refreshToken: string } | { type: 'line'; idToken: string; lineAccessToken: string; authorizationCode: string }; ``` **Actions:** ```typescript type AuthActions = { // State setters setIsLoading(value: boolean): void; setIsLoadingToken(value: boolean): void; setError(value: unknown): void; setThirdPartyLoginData(data: ThirdPartyLoginData): void; // Check persisted token and restore session checkLoggedIn(): Promise<void>; // Login methods all return ServiceResponse<LoginResponse> loginWithUsernamePassword(username: string, password: string): Promise<ServiceResponse<LoginResponse>>; loginWithGoogle(token: string): Promise<ServiceResponse<LoginResponse>>; loginWithFacebook(token: string): Promise<ServiceResponse<LoginResponse>>; loginWithApple(token: string, refreshToken: string): Promise<ServiceResponse<LoginResponse>>; loginWithLine(idToken: string, lineAccessToken: string, authorizationCode: string): Promise<ServiceResponse<LoginResponse>>; loginWithUUID(): Promise<ServiceResponse<LoginResponse>>; loginWithOtp(otp: string, refCode: string, contact: string): Promise<ServiceResponse<LoginResponse>>; // Account connection link an already-authenticated account to a third-party identity connectThirdParty(data: ThirdPartyLoginData): Promise<ServiceResponse<LoginResponse>>; // Password management forgetPassword(contact: string, type: 'email' | 'contact_number'): Promise<ServiceResponse<ForgetPasswordResponse>>; resetPassword(contact: string, otp: string, refCode: string, password: string): Promise<ServiceResponse<StatusResponse>>; // OTP sendOtp(contact: string, channel: string): Promise<ServiceResponse<OtpResponse>>; validateOtp(otp: string, refCode: string, contact: string, channel: string, type: 'email' | 'contact_number'): Promise<ServiceResponse<ValidateOtpResponse>>; confirmOtp(otp: string, refCode: string, contact: string): Promise<ServiceResponse<ConfirmOtpResponse>>; // Apple Sign-In token exchange appleToken(authorizationCode: string, idToken: string): Promise<ServiceResponse<AppleToken>>; // Session management logout(): Promise<ServiceResponse<unknown>>; resume(clientVersion: string): Promise<ServiceResponse<ResumeResponse>>; version(clientVersion: string): Promise<ServiceResponse<Version>>; clear(): void; }; ``` **Example:** ```typescript const { isLoggedIn, isLoading, token, actions } = useAuthStore(); // Login const res = await actions.loginWithUsernamePassword('user@example.com', 'secret'); if (res.type === 'success') { /* navigate to home */ } // Third-party login await actions.loginWithGoogle(googleIdToken); await actions.loginWithLine(lineIdToken, lineAccessToken, authorizationCode); // If login failed because the third-party identity is not yet linked, // authenticate the user some other way and then connect the identity: await actions.connectThirdParty({ type: 'google', idToken: googleIdToken }); // Check token on app start useEffect(() => { actions.checkLoggedIn(); }, []); // Logout await actions.logout(); ``` > **Behavior:** Each `loginWith*` call records its identity in `thirdPartyLoginData` immediately (before the API call), so the UI can read which identity is being attempted. On a successful `connectThirdParty`, the field is cleared. > > After a successful login, `useAuthStore` automatically calls `useUserStore.getState().fetchUser()` and fires analytics events. --- ### useUserStore Manages the authenticated user's profile, points, point-service balances, expiring points, badges, cart count, and traces. ```typescript import { useUserStore } from '@bzbs/react-providers/zustand'; ``` **State:** ```typescript type UserState = { user: ProfileResponse | null; points: number; // Regular point balance pointServiceBalance: { [unit: string]: number }; // Per-unit balances from point-service expiringPointService: { // Per-unit expiring schedule [unit: string]: { date: string; points: number }[]; }; recentPoints: number; expiringPoints: UpdatedPoints[]; recentBadge: Badge | null; recentTrace: Trace | null; cartCount: number; isLoading: boolean; error: ErrorResponse | null; imageError: ErrorResponse | null; // Errors from profile-image updates badgeList: Badge[]; traceList: Trace[]; }; ``` **Actions:** ```typescript type UserActions = { fetchUser(): Promise<ServiceResponse<ProfileResponse>>; // Fetches both regular points AND point-service balances in parallel fetchPoint(): Promise<{ pointResponse: ServiceResponse<UpdatedPoints>; customPointsResponse: ServiceResponse<PointBalance[]>; }>; fetchExpiringPoints(): Promise<ServiceResponse<ExpiringPoints>>; fetchCartCount(): Promise<ServiceResponse<CartCountResponse>>; editUser(params: UpdateProfileParams): Promise<ServiceResponse<ProfileResponse>>; changePassword(oldPassword: string, newPassword: string): Promise<ServiceResponse<unknown>>; changeContactNumber(contactNumber: string, otp: string, refCode: string): Promise<ServiceResponse<unknown>>; changeAvatar(file: File | Blob): Promise<ServiceResponse<unknown>>; deleteUser(): Promise<ServiceResponse<unknown>>; consumeRecentPoints(): void; consumeRecentBadge(): void; consumeRecentTrace(): void; setupEventListeners(): void; cleanupEventListeners(): void; setError(value: unknown): void; clear(): void; }; ``` > **Profile image:** `changeAvatar` and `editUser` (when called with `profileImage`) now use the dedicated `profileApi.updateProfileImage` endpoint. Image-upload failures are reported on `imageError` rather than `error`, so the profile-edit success state is not blocked by an avatar failure. --- ### useCartStore Fetches and tracks the number of items in the user's cart. ```typescript import { useCartStore } from '@bzbs/react-providers/zustand'; ``` **State:** ```typescript type CartState = { count: number; isLoading: boolean; error: ErrorResponse | null; }; ``` **Actions:** ```typescript type CartActions = { fetchCount(): Promise<ServiceResponse<CartCountResponse>>; clear(): void; }; ``` --- ### useLocaleStore Manages the app locale used for API requests. ```typescript import { useLocaleStore, EN, TH } from '@bzbs/react-providers/zustand'; ``` **State:** ```typescript type LocaleState = { appLocale: AppLocale; locale: string; // e.g. 'en', 'th' localeId: number; // LCID e.g. 1033, 1054 }; type AppLocale = { locale: string; localeId: number; }; ``` **Constants:** ```typescript const EN: AppLocale = { locale: 'en', localeId: 1033 }; const TH: AppLocale = { locale: 'th', localeId: 1054 }; ``` **Actions:** ```typescript type LocaleActions = { // Accepts an AppLocale object or a locale string like 'en', 'th-TH', 'en-US' setAppLocale(locale: AppLocale | string): void; }; ``` **Example:** ```typescript const { locale, localeId } = useLocaleStore(); // Set by locale string (resolves via findBestLocaleFromString) useLocaleStore.getState().setAppLocale('th'); // Set by full AppLocale object useLocaleStore.getState().setAppLocale(TH); ``` --- ### useAnalyticsStore Manages Matomo analytics tracking. ```typescript import { useAnalyticsStore } from '@bzbs/react-providers/zustand'; ``` **State:** ```typescript type AnalyticsState = { isInitialized: boolean; isEnabled: boolean; siteId: number; urlBase: string; trackerUrl: string | undefined; userId: string | undefined; disabled: boolean; log: boolean; matomoInstance: MatomoTracker | null; presetUserInfo: UserInfo | undefined; }; ``` **Actions:** ```typescript type AnalyticsActions = { initialize(config: { siteId: number; urlBase: string; trackerUrl?: string; userId?: string; disabled?: boolean; log?: boolean; }): void; setEnabled(enabled: boolean): void; setPresetUserInfo(userInfo: UserInfo): void; clearPresetUserInfo(): void; trackAppStart(userInfo?: UserInfo): Promise<void>; trackEvent(params: { action: string; name?: string; category?: string; value?: number; campaign?: string; userInfo?: UserInfo; }): Promise<void>; trackScreenView(params: { name: string; userInfo?: UserInfo }): Promise<void>; trackAction(params: { name: string; userInfo?: UserInfo }): Promise<void>; trackSiteSearch(params: { keyword: string; category?: string; count?: number; userInfo?: UserInfo; }): Promise<void>; trackLink(params: { link: string; userInfo?: UserInfo }): Promise<void>; trackDownload(params: { download: string; userInfo?: UserInfo }): Promise<void>; setUserId(userId: string | null): void; setCustomDimension(id: number, value: string): void; reset(): void; }; ``` --- ### useAlertStore Manages alert/dialog display state. ```typescript import { useAlertStore } from '@bzbs/react-providers/zustand'; ``` **State:** ```typescript type AlertState = { isOpen: boolean; request: AlertRequest | null; }; type AlertRequest = { title?: string; message: string; confirmLabel?: string; onConfirm?: () => void; }; ``` **Actions:** ```typescript type AlertActions = { show(request: AlertRequest): void; hide(): void; }; ``` --- ### useConfirmStore Manages confirmation dialog state. ```typescript import { useConfirmStore } from '@bzbs/react-providers/zustand'; ``` **State:** ```typescript type ConfirmState = { isOpen: boolean; request: ConfirmRequest | null; }; type ConfirmRequest = { title?: string; message: string; confirmLabel?: string; cancelLabel?: string; onConfirm?: () => void; onCancel?: () => void; }; ``` **Actions:** ```typescript type ConfirmActions = { show(request: ConfirmRequest): void; hide(): void; }; ``` --- ### usePopupStore Manages popup/modal display state. ```typescript import { usePopupStore } from '@bzbs/react-providers/zustand'; ``` **State:** ```typescript type PopupState = { isOpen: boolean; request: PopupRequest | null; }; type PopupRequest = { content: React.ReactNode; onClose?: () => void; }; ``` **Actions:** ```typescript type PopupActions = { show(request: PopupRequest): void; hide(): void; }; ``` --- ### useLoadingIndicatorStore Manages a global loading indicator with named loading keys. ```typescript import { useLoadingIndicatorStore, useLoading } from '@bzbs/react-providers/zustand'; ``` **State:** ```typescript type LoadingIndicatorState = { loadingKeys: string[]; isLoading: boolean; }; ``` **Actions:** ```typescript type LoadingIndicatorActions = { show(key?: string): void; hide(key?: string): void; clear(): void; }; ``` **`useLoading` hook:** A convenience hook for reading the current loading state. --- ### useNotificationStore Manages user notification list and unread count. ```typescript import { useNotificationStore } from '@bzbs/react-providers/zustand'; ``` Wraps `notificationApi` from `@bzbs/react-api-client`. Actions include fetching notifications, marking as read, and clearing state. --- ### useMaintenanceStore Manages app maintenance mode state. ```typescript import { useMaintenanceStore } from '@bzbs/react-providers/zustand'; ``` Fetches maintenance configuration from the API and exposes `isMaintenance: boolean` and `maintenanceData`. --- ### useConsentStore Manages user consent preferences (marketing, data sharing, etc.). ```typescript import { useConsentStore } from '@bzbs/react-providers/zustand'; ``` Wraps `consentApi` from `@bzbs/react-api-client`. Exposes consent state and update actions. --- ### useRegistrationStore Manages user registration flow state. ```typescript import { useRegistrationStore } from '@bzbs/react-providers/zustand'; ``` Wraps `registrationApi`. Handles multi-step registration: form data, OTP verification, and account creation. --- ### useAddressStore Manages user address book. ```typescript import { useAddressStore } from '@bzbs/react-providers/zustand'; ``` Wraps `addressApi`. CRUD operations for user addresses. --- ### useZipCodeStore Fetches postal/zip code data for address forms. ```typescript import { useZipCodeStore } from '@bzbs/react-providers/zustand'; ``` --- ### createCampaignsStore Factory that creates a scoped campaigns list store. Use when you need multiple independent campaign list instances. ```typescript import { createCampaignsStore, type CampaignsOptions } from '@bzbs/react-providers/zustand'; const useMyCampaignsStore = createCampaignsStore({ defaultCampaignConfig: 'home' }); ``` **Actions:** ```typescript type CampaignsActions = { fetchData(categoryId?: string, options?: CampaignsOptions): Promise<ServiceResponse<Campaign[]>>; loadMore(): Promise<ServiceResponse<Campaign[]>>; clear(): void; updateConfig(config: Partial<CampaignsConfig>): void; }; ``` **`CampaignsOptions`** Typed filter/sort/search options accepted by `fetchData`: ```typescript type CampaignsOptions = { keyword?: string; startDate?: string; sponsorId?: string; maxPoints?: string; minPoints?: string; minPrice?: string; maxPrice?: string; sortBy?: string; center?: string; hashTags?: string; locationAgencyId?: string; campaignservice?: boolean; mode?: 'hot' | 'all' | 'bzbs' | 'sponsor' | 'draw' | 'free' | 'deal' | 'buy' | 'ads' | 'full' | 'cart' | string; [key: string]: unknown; }; ``` --- ### createCampaignDetailStore Factory that creates a scoped campaign detail store. ```typescript import { createCampaignDetailStore } from '@bzbs/react-providers/zustand'; const useCampaignDetailStore = createCampaignDetailStore(); ``` Wraps `campaignApi.getCampaignDetail()` and `campaignApi.redeem()`. Computes a `buttonState` from the campaign type, point requirements, sold-out status, time window, and (when `supportPointUnits` is configured) per-unit point-service balances. **Redeem signature:** ```typescript redeem( addressOverride?: Address, pointUnit?: string, // Which point-service unit to spend (when supportPointUnits is set) options?: { [key: string]: unknown } ): Promise<ServiceResponse<RedeemResponse>>; ``` When `pointUnit` is provided, the redemption uses `data.PointServices[pointUnit].AmountPerUnit` as the cost rather than `data.PointPerUnit`. > **Condition checks:** Point-sufficiency is *not* evaluated for `buy`, `event`, `media`, or `news` campaigns. For all other types, `isPointEnough` is consulted against `points` + `pointServiceBalance`. --- ### createCategoriesStore Factory that creates a scoped categories/menu store. ```typescript import { createCategoriesStore } from '@bzbs/react-providers/zustand'; const useCategoriesStore = createCategoriesStore(); ``` --- ### createDashboardStore Factory that creates a scoped dashboard configuration store. ```typescript import { createDashboardStore } from '@bzbs/react-providers/zustand'; const useDashboardStore = createDashboardStore(); ``` --- ### createPointLogStore Factory that creates a scoped point history store. ```typescript import { createPointLogStore } from '@bzbs/react-providers/zustand'; const usePointLogStore = createPointLogStore(); ``` Wraps `pointLogApi`. Supports pagination. --- ### createPurchaseStore Factory that creates a scoped purchase history store. ```typescript import { createPurchaseStore } from '@bzbs/react-providers/zustand'; const usePurchaseStore = createPurchaseStore(); ``` --- ### useCouponStore Manages user coupon list and coupon operations. ```typescript import { useCouponStore } from '@bzbs/react-providers/zustand'; ``` --- ### createFavoriteCampaignsStore Factory that creates a scoped favorites store. ```typescript import { createFavoriteCampaignsStore } from '@bzbs/react-providers/zustand'; const useFavoriteCampaignsStore = createFavoriteCampaignsStore(); ``` --- ## TokenFunctions Interface A required interface for token persistence. You provide your own implementation based on your storage mechanism. ```typescript type TokenFunctions = { getToken: () => Promise<string | null>; setToken: (token: string) => void; removeToken: () => void; }; ``` **Example implementations:** ```typescript // Web (localStorage) const tokenFunctions: TokenFunctions = { getToken: async () => localStorage.getItem('auth_token'), setToken: (token) => localStorage.setItem('auth_token', token), removeToken: () => localStorage.removeItem('auth_token'), }; // React Native (AsyncStorage) import AsyncStorage from '@react-native-async-storage/async-storage'; const tokenFunctions: TokenFunctions = { getToken: () => AsyncStorage.getItem('auth_token'), setToken: (token) => AsyncStorage.setItem('auth_token', token), removeToken: () => AsyncStorage.removeItem('auth_token'), }; // In-memory (testing / SSR) let _token: string | null = null; const tokenFunctions: TokenFunctions = { getToken: async () => _token, setToken: (token) => { _token = token; }, removeToken: () => { _token = null; }, }; ``` --- ## Service Utilities Exported from `@bzbs/react-providers/zustand`. --- ### addDefaultHeaderInterceptor Attaches request and response interceptors to an Axios instance. **Call this once** after creating your Axios instance and before passing it to `BzbsService`. ```typescript import { addDefaultHeaderInterceptor } from '@bzbs/react-providers/zustand'; addDefaultHeaderInterceptor( axiosInstance: AxiosInstance, appId: string, subscriptionKey: string, tokenFunctions: TokenFunctions, apiVersion: string, corelationIdGenerator?: () => string // optional, defaults to uuidv7() ): void ``` **What it does:** | Direction | Action | |---|---| | Request | Adds `App-Id`, `Subscription-Key`, `Api-Version`, `X-Corelation-Id` headers | | Request | Adds `Authorization: Bearer <token>` (JWT) or `Authorization: token <token>` | | Response | Normalises both new-style (`{ Success, Data }`) and old-style (`{ error, ... }`) responses | | Response | Emits `get_points` event if response contains `buzzebees.points` | | Response | Emits `get_badges` event if response contains `buzzebees.badges` | | Response | Emits `session_expired` event on error code `1905` or `2076` | > **Header rename (2.8.0):** `Ocp-Apim-Subscription-Key` `Subscription-Key`, and `api-version` `Api-Version`. --- ### addBlake2OtpSignatureInterceptor Adds BLAKE2b OTP signature headers to requests matching specified URL paths. ```typescript import { addBlake2OtpSignatureInterceptor } from '@bzbs/react-providers/zustand'; addBlake2OtpSignatureInterceptor( axiosInstance: AxiosInstance, paths: string[], // URL path substrings to match (e.g. ['/otp/send']) encoder: TextEncoder, getCurrentDateTime?: () => Date // optional, defaults to () => new Date() ): void ``` When a matching request includes `app_id`, `contact_number`, and `channel` params, the interceptor adds: - `OTP-Signature`: BLAKE2b hex of `appId|contactNumber|channel|datetime` - `Timestamp`: Unix timestamp (seconds) --- ### Utility Functions ```typescript import { avatarUrl, largeImage, fetchImage, interfaceWebsite, createInterfaceToken, createCartUrl, generateBlake2bSignatureHex, generateShortLivedId, getOtpSignatureHeaders, getProfileImageSource, } from '@bzbs/react-providers/zustand'; ``` | Function | Signature | Description | |---|---|---| | `avatarUrl` | `(baseUrl, appId, userId, token) => string` | Builds a profile picture URL with cache-busting timestamp | | `largeImage` | `(url) => string` | Transforms a standard image URL to its large variant | | `fetchImage` | `(axiosClient, imageUrl) => Promise<Blob \| undefined>` | Downloads an image as a Blob | | `interfaceWebsite` | `(url, token?, returnUrl?, params?) => string` | Builds an interface website URL with obfuscated token | | `createInterfaceToken` | `(token) => string` | Obfuscates a token by rearranging first/last characters | | `createCartUrl` | `(cartUrl, appName, accessKey, params?) => string` | Builds a cart landing URL with access key | | `generateBlake2bSignatureHex` | `(data, key, encoder) => string` | Generates a BLAKE2b hex signature | | `generateShortLivedId` | `() => string` | Generates a random short-lived ID string | | `getOtpSignatureHeaders` | `({ appId, contactNumber, channel, encoder, now? }) => object` | Returns `{ 'OTP-Signature': string, Timestamp: number }` | | `getProfileImageSource` | `(baseUrl, appId, subscriptionKey, apiVersion, token, corelationIdGenerator?, timestamp?, type?) => ProfileImageSource` | Builds a `{ uri, headers }` source for `<Image>` components fetching the authenticated profile picture | **`getProfileImageSource`** is intended for React Native `<Image source={...} />` where the URL needs to be fetched with full Buzzebees auth headers: ```typescript const source = getProfileImageSource( 'https://api.buzzebees.com', 'YOUR_APP_ID', 'YOUR_SUBSCRIPTION_KEY', '1.0', authToken ); // source = { uri: 'https://.../profile/picture?timestamp=...&type=large', headers: { ... } } <Image source={source} /> ``` --- ### eventEmitter A global `Emittery` instance for cross-component communication. The `addDefaultHeaderInterceptor` automatically fires events on it. ```typescript import { eventEmitter } from '@bzbs/react-providers/zustand'; // Listen for points update (response contains buzzebees.points) eventEmitter.on('get_points', (data) => { console.log('New points:', data.points); }); // Listen for badges update eventEmitter.on('get_badges', (badges) => { console.log('New badges:', badges); }); // Listen for session expiry (error code 1905 or 2076) eventEmitter.on('session_expired', (error) => { // Redirect to login, clear token, etc. useAuthStore.getState().clear(); }); ``` > `useUserStore` internally calls `setupEventListeners()` to subscribe to `get_points` and `get_badges` and update its state. Call `cleanupEventListeners()` on unmount. --- ## Analytics Utilities ```typescript import { analytics, trackCommonEvents, useAnalytics, usePageTracking, useAnalyticsState, initializeAnalytics, withPageTracking, } from '@bzbs/react-providers/zustand'; ``` --- ### trackCommonEvents Pre-built Matomo event calls for common Buzzebees actions. All functions are `async` and no-op if analytics is not initialized. ```typescript await trackCommonEvents.loginSuccess(userInfo?) await trackCommonEvents.registerSuccess(userInfo?) await trackCommonEvents.viewCampaign(userInfo?) await trackCommonEvents.redeemSuccess(campaignId: string, userInfo?) await trackCommonEvents.unconsentSuccess(userInfo?) await trackCommonEvents.buttonClick(buttonName: string, category?: string, userInfo?) await trackCommonEvents.formSubmit(formName: string, success?: boolean, userInfo?) await trackCommonEvents.search(keyword: string, resultCount?: number, category?: string, userInfo?) await trackCommonEvents.purchase(amount: number, currency?: string, productName?: string, userInfo?) await trackCommonEvents.userRegistration(method?: string, userInfo?) await trackCommonEvents.userLogin(method?: string, userInfo?) await trackCommonEvents.error(errorType: string, errorMessage?: string, userInfo?) await trackCommonEvents.custom(name: string, category: string, label?: string, value?: number, userInfo?) ``` --- ### analytics (global object) For use **outside React components** (service files, event handlers, etc.). ```typescript analytics.init({ siteId: 1, urlBase: 'https://matomo.example.com' }) analytics.trackEvent({ action, name?, category?, value?, campaign?, userInfo? }) analytics.trackScreenView(name: string, userInfo?) analytics.trackAppStart(userInfo?) analytics.trackSiteSearch(keyword: string, category?, count?, userInfo?) analytics.trackLink(link: string, userInfo?) analytics.trackDownload(download: string, userInfo?) analytics.setUserId(userId: string | null) analytics.setCustomDimension(id: number, value: string) analytics.setPresetUserInfo(userInfo: UserInfo) analytics.clearPresetUserInfo() analytics.setEnabled(enabled: boolean) analytics.reset() ``` --- ### useAnalytics Hook React hook that returns analytics functions from the current context. ```typescript const { isInitialized, isEnabled, trackEvent, trackScreenView, trackAction, trackAppStart, trackSiteSearch, trackLink, trackDownload, setUserId, setCustomDimension, setEnabled, setPresetUserInfo, clearPresetUserInfo, } = useAnalytics(); ``` **`usePageTracking` hook** automatically tracks screen view on mount: ```typescript usePageTracking( pageName?: string, dependencies?: unknown[], customUserInfo?: UserInfo ): void ``` **`withPageTracking` HOC:** ```typescript const TrackedComponent = withPageTracking(MyComponent, 'Home Screen'); ``` --- ## Locale Utilities ```typescript import { findBestLocaleFromString, localeToLCIDMap } from '@bzbs/react-providers/zustand'; ``` **`findBestLocaleFromString(value: string): { locale: string; localeId: number }`** Resolves a locale string to an `AppLocale` object with LCID. Supports: - Exact match: `'th-TH'` `{ locale: 'th-TH', localeId: 1054 }` - Partial match: `'th'` finds first key starting with `'th'` - Throws `Error` if no match found **`localeToLCIDMap`** Object mapping 200+ locale strings (BCP 47) to Windows LCID numbers. Common mappings: | Locale | LCID | |---|---| | `en-US` | 1033 | | `th-TH` | 1054 | | `zh-CN` | 2052 | | `zh-TW` | 1028 | | `ja-JP` | 1041 | | `ko-KR` | 1042 | | `ms-MY` | 1086 | | `id-ID` | 1057 | | `vi-VN` | 1066 | --- ## Campaign Utilities ```typescript import { isCodeAutoUse, isPointEnough } from '@bzbs/react-providers/zustand'; ``` **`isCodeAutoUse(campaign: CampaignDetail, redeemData: RedeemResponse): boolean`** Determines whether a redeemed campaign code was auto-consumed. Returns `campaign.IsRequireUniqueSerial === true` when `IsNotAutoUse` is set, otherwise `redeemData.IsUsed === true`. **`isPointEnough(campaign, userPoints, pointServiceBalance, supportPointUnits, quantity?): boolean`** Computes whether the user can afford a campaign redemption, taking the point-service feature into account: | Condition | Result | |---|---| | `campaign.PointType === 'get'` | `true` (campaign awards points; never blocked) | | `supportPointUnits` is empty | Compares `userPoints` against `campaign.PointPerUnit × quantity` | | `campaign.PointServices` is missing | Falls back to regular `userPoints` comparison | | Otherwise | `true` if any unit in `supportPointUnits` has `pointServiceBalance[unit] >= PointServices[unit].AmountPerUnit × quantity` | This is the same helper consulted internally by `createCampaignDetailStore`'s `updateButtonState` and `checkCondition`. --- ## Constants ```typescript import { campaignType, campaignPointType, campaignInterfaceType } from '@bzbs/react-providers/zustand'; ``` --- ### campaignType Maps campaign type names to numeric IDs used by the Buzzebees API. ```typescript const campaignType = { draw: 0, free: 1, deal: 2, buy: 3, bid: 4, ads: 5, install: 6, booking: 7, interface: 8, event: 9, media: 10, ewalletTopUp: 11, ewalletRedeem: 12, ewalletTransfer: 13, ewalletBanking: 14, autofeed: 15, news: 16, reservation: 17, ewalletBuy: 18, pointRedemption: 19, donate: 20, pointFree: 21, voucher: 22, encrypt: 23, encryptRedeem: 24, giftCard: 25, verifyCode: 26, subscription: 27, fillCode: 28, subscriptionFood: 29, buyEVoucher: 30, uploadReceipt: 31, payWithPoints: 32, marketPlacePrivilege: 33, topup2C2P: 34, directTopup2C2P: 35, }; ``` **Usage:** ```typescript if (campaign.campaign_type === campaignType.voucher) { // Handle voucher campaign } ``` --- ### campaignPointType ```typescript const campaignPointType = { use: 'use', // campaign costs points get: 'get', // campaign rewards points }; ``` --- ### campaignInterfaceType ```typescript const campaignInterfaceType = { web: 'web', survey: 'survey', surveyApprove: 'surveyapprove', }; ``` --- ## Types Reference ```typescript import type { TokenFunctions, AuthTokenType, AppLocale, MatomoConfig, LoginResponseHandler, ErrorResponseHandler, ProfileImageSource, // Store types BuzzebeesAppState, BuzzebeesAppActions, BuzzebeesAppStore, AuthState, AuthActions, AuthStore, ThirdPartyLoginData, GoogleLoginData, FacebookLoginData, AppleLoginData, LineLoginData, UserState, UserActions, UserStore, CartState, CartActions, CartStore, LocaleState, LocaleActions, LocaleStore, AlertState, AlertActions, AlertStore, AlertRequest, ConfirmState, ConfirmActions, ConfirmStore, ConfirmRequest, PopupState, PopupActions, PopupStore, PopupRequest, LoadingIndicatorState, LoadingIndicatorActions, LoadingIndicatorStore, UpdateProfileParams, CampaignsOptions, CampaignsConfig, } from '@bzbs/react-providers/zustand'; ``` | Type | Description | |---|---| | `TokenFunctions` | Token persistence interface (getToken/setToken/removeToken) | | `AuthTokenType` | `'jwt' \| 'auth_token'` token format stored after login | | `AppLocale` | `{ locale: string; localeId: number }` | | `MatomoConfig` | Matomo tracker configuration (`siteId`, `urlBase`, `trackerUrl?`, `userId?`, `disabled?`, `log?`) | | `LoginResponseHandler` | `(response: LoginResponse) => void` | | `ErrorResponseHandler` | `(error: ErrorResponse) => void` | | `ProfileImageSource` | `{ uri: string; headers: { [key: string]: unknown } }` return shape of `getProfileImageSource` | | `ThirdPartyLoginData` | Discriminated union of `GoogleLoginData \| FacebookLoginData \| AppleLoginData \| LineLoginData` | | `UpdateProfileParams` | Parameters for `useUserStore().editUser()` | | `CampaignsOptions` | Filter/sort/search options passed to `createCampaignsStore` `fetchData` | All API response types (`LoginResponse`, `ProfileResponse`, `ErrorResponse`, `PointBalance`, `CampaignDetail`, etc.) are re-exported from `@bzbs/react-api-client`. See [react-api-client.md](./react-api-client.md) for the full models reference. --- ## Relationship to @bzbs/react-api-client `@bzbs/react-providers` is the **stateful wrapper** around `@bzbs/react-api-client`. The relationship: ``` @bzbs/react-api-client @bzbs/react-providers ──────────────────────── ────────────────────────────── BzbsService Passed into useBuzzebeesAppStore authApi useAuthStore profileApi useUserStore pointApi useUserStore (pointServiceBalance, expiringPointService) cartApi useCartStore campaignApi createCampaignsStore / createCampaignDetailStore categoryApi createCategoriesStore couponApi useCouponStore notificationApi useNotificationStore historyApi createPurchaseStore registrationApi useRegistrationStore addressApi useAddressStore badgeApi useUserStore (badges) consentApi useConsentStore dashboardApi createDashboardStore pointLogApi createPointLogStore ``` To create a `BzbsService` instance, refer to [`react-api-client.md`](./react-api-client.md). --- ## Migration from 2.x The `3.0.0` release removed the Context API entirely. If you are upgrading from a `2.x` version: | Removed (2.x) | Replacement (3.x) | |---|---| | `DefaultMainAppProvider` (wrapping the whole app) | Call `useBuzzebeesAppStore.getState().configure({ ... })` once at app startup | | `import { ... } from '@bzbs/react-providers/context'` | `import { ... } from '@bzbs/react-providers/zustand'` | | `import { ... } from '@bzbs/react-providers'` | Now equivalent to `/zustand` only that entry point is built | | `useBuzzebeesServiceContext()` | `useBuzzebeesAppStore()` | | `useAuthContext()` | `useAuthStore()` | | `useUserContext()` | `useUserStore()` | | `useCartContext()` | `useCartStore()` | | `useCampaignsContext()` | `createCampaignsStore()` | | `useCampaignDetailContext()` | `createCampaignDetailStore()` | | `useCategoriesContext()` | `createCategoriesStore()` | | `useCouponContext()` | `useCouponStore()` | | `useAlertContext()` | `useAlertStore()` | | `useConfirmContext()` | `useConfirmStore()` | | `usePopupContext()` | `usePopupStore()` | | `useLoadingContext()` / `useLoading()` | `useLoadingIndicatorStore()` / `useLoading()` | | `useNotificationContext()` | `useNotificationStore()` | | `useAnalyticsContext()` | `useAnalyticsStore()` | | `useAppLocaleContext()` | `useLocaleStore()` | | `useConsentContext()` | `useConsentStore()` | | `useMaintenanceContext()` | `useMaintenanceStore()` | | `useRegistrationContext()` | `useRegistrationStore()` | | `useAddressContext()` | `useAddressStore()` | | `useZipCodeContext()` | `useZipCodeStore()` | | `useDashboardContext()` | `createDashboardStore()` | | `usePointLogContext()` | `createPointLogStore()` | | `usePurchaseContext()` | `createPurchaseStore()` | Other notable breaking changes: - **2.8.0** Default headers renamed: `Ocp-Apim-Subscription-Key` `Subscription-Key`, `api-version` `Api-Version`. If you set these headers manually anywhere, update them. - **2.8.0** `useUserStore.changeAvatar` and `editUser({ profileImage })` now call `profileApi.updateProfileImage` and report failures on the new `imageError` state field. - **3.0.0** `useBuzzebeesAppStore.config` requires a new field `supportPointUnits: string[]` (use `[]` if unused). - **3.0.0** `useUserStore.fetchPoint()` now returns `{ pointResponse, customPointsResponse }` instead of `ServiceResponse<UpdatedPoints>`. - **3.0.0** `createCampaignDetailStore` `redeem(addressOverride?, options?)` is now `redeem(addressOverride?, pointUnit?, options?)`. Pass `undefined` for `pointUnit` if you don't use point-service. - **3.0.4** `useUserStore.customPoints` was renamed to `useUserStore.pointServiceBalance`; expiring data is exposed via `expiringPointService`. --- ## Development Commands ```bash # Build the /zustand entry point (dist/) npm run build # Run tests with coverage npm test # Format source files npm run format # Upgrade @bzbs/react-api-client to latest npm run update:client # Publish — patch version bump npm run patch # Publish — minor version bump npm run minor # Publish — major version bump npm run major ``` Build output (single entry point, CJS + ESM + types): | Entry point | CJS | ESM | Types | |---|---|---|---| | `zustand.ts` | `dist/zustand.js` | `dist/zustand.mjs` | `dist/zustand.d.ts` |