@bzbs/react-providers
Version:
A collection of React Context Provider for Buzzebees apps
1,534 lines (1,210 loc) • 44.9 kB
Markdown
# @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` |