import { NativeEventEmitter, Platform } from 'react-native'; import { FWEventName } from './models/FWEventName'; import type { ClickProductEvent, CustomClickCartIconEvent, CustomClickLinkButtonEvent, CustomTapProductCardEvent, ShoppingCTAEvent, UpdateProductDetailsEvent, } from './models/FWEvents'; import type Product from './models/Product'; import type ProductInfoViewConfiguration from './models/ProductInfoViewConfiguration'; import ShoppingModule, { ShoppingModuleEventEmitter, } from './modules/ShoppingModule'; import FWLoggerUtil from './utils/FWLoggerUtil'; import type ShoppingCTAResult from './models/ShoppingCTAResult'; import FWGlobalState from './utils/FWGlobalState'; export type ShoppingCTACallback = ( event: ShoppingCTAEvent ) => Promise | ShoppingCTAResult; export type CustomClickCartIconCallback = ( event: CustomClickCartIconEvent ) => Promise | void; export type UpdateProductDetailsCallbackResult = Product[] | undefined | null; export type UpdateProductDetailsCallback = ( event: UpdateProductDetailsEvent ) => | Promise | UpdateProductDetailsCallbackResult; export type CustomClickLinkButtonCallback = ( event: CustomClickLinkButtonEvent ) => Promise | void; export type CustomTapProductCardCallback = ( event: CustomTapProductCardEvent ) => Promise | void; export type ClickProductCallback = ( event: ClickProductEvent ) => Promise | void; /** * The entry class of video shopping. */ class VideoShopping { private static _instance?: VideoShopping; /** * This callback is triggered when the user clicks the "Add to cart" or "Shop now" button. * The host app can return a ShoppingCTAResult object to tell SDK how to handle the result. */ public onShoppingCTA?: ShoppingCTACallback; /** * This callback is triggered when the user clicks the shopping cart icon. * * The host app can customize the click event processing logic of * the shopping cart icon by setting the callback. */ public onCustomClickCartIcon?: CustomClickCartIconCallback | undefined; /** * This callback is triggered when the video will be shown. * * The host app can return a Product list to update the latest product information. */ public onUpdateProductDetails?: UpdateProductDetailsCallback; /** * This callback is triggered when the user clicks * the link button next to Add to cart button. * * The host app can customize the click event processing logic of * the link button by setting the callback. */ public get onCustomClickLinkButton(): | CustomClickLinkButtonCallback | undefined { return this._onCustomClickLinkButton; } public set onCustomClickLinkButton( value: CustomClickLinkButtonCallback | undefined ) { this._onCustomClickLinkButton = value; ShoppingModule.setCustomClickLinkButtonEnabled(!!value); } private _onCustomClickLinkButton?: CustomClickLinkButtonCallback | undefined; /** * This callback is triggered when the user clicks product card. * * The host app can customize the tap event processing logic of * the product card by setting the callback. */ public get onCustomTapProductCard(): | CustomTapProductCardCallback | undefined { return this._onCustomTapProductCard; } public set onCustomTapProductCard( value: CustomTapProductCardCallback | undefined ) { this._onCustomTapProductCard = value; ShoppingModule.setCustomTapProductCardEnabled(!!value); } private _onCustomTapProductCard?: CustomTapProductCardCallback | undefined; /** * This callback is triggered when the user clicks product card. * Only supported on iOS */ public onClickProduct?: ClickProductCallback; /** * Defaults to true. * You can hide the cart icon by setting this property to false. */ public get cartIconVisible(): boolean { return this._cartIconVisible; } public set cartIconVisible(value: boolean) { this._cartIconVisible = value; if (FWGlobalState.getInstance().sdkInitSucceeded) { ShoppingModule.setCartIconVisible(value); } else { FWGlobalState.getInstance().sdkInitSucceededPromise.then(() => { ShoppingModule.setCartIconVisible(value); }); } } private _cartIconVisible: boolean = true; /** * The configuration of product info view. * Please refer to {@link ProductInfoViewConfiguration} for more details. */ public get productInfoViewConfiguration(): | ProductInfoViewConfiguration | undefined { return this._productInfoViewConfiguration; } public set productInfoViewConfiguration( value: ProductInfoViewConfiguration | undefined ) { const valueHasChanged = this._productInfoViewConfiguration !== value; this._productInfoViewConfiguration = value; if (FWGlobalState.getInstance().sdkInitSucceeded) { this.updateNativeProductInfoViewConfiguration(value, valueHasChanged); } else { FWGlobalState.getInstance().sdkInitSucceededPromise.then(() => { this.updateNativeProductInfoViewConfiguration(value, valueHasChanged); }); } } private updateNativeProductInfoViewConfiguration( value: ProductInfoViewConfiguration | undefined, valueHasChanged: Boolean ) { ShoppingModule.setProductInfoViewConfiguration(value ?? {}).then(() => { if (valueHasChanged) { this.eventEmitter.emit(FWEventName.ProductInfoViewConfigurationUpdated); FWLoggerUtil.log('eventEmitter ProductInfoViewConfigurationUpdated'); } }); } private _productInfoViewConfiguration?: | ProductInfoViewConfiguration | undefined; private get eventEmitter(): NativeEventEmitter { return ShoppingModuleEventEmitter; } public static getInstance() { FWLoggerUtil.log('VideoShopping constructor'); if (!VideoShopping._instance) { VideoShopping._instance = new VideoShopping(); } return VideoShopping._instance!; } private constructor() { this.eventEmitter.addListener( FWEventName.ShoppingCTAButtonClick, (event) => { FWLoggerUtil.log( `Receive ShoppingCTA event productId: ${event?.productId} unitId: ${event?.unitId} url: ${event?.url}` ); this.handleShoppingCTAEvent(event); } ); this.eventEmitter.addListener(FWEventName.ClickCartIcon, (event) => { FWLoggerUtil.log('Receive ClickCartIcon event'); this.handleClickCartIconEvent(event); }); this.eventEmitter.addListener(FWEventName.UpdateProductDetails, (event) => { FWLoggerUtil.log( `Receive UpdateProductDetails event productIds: ${event?.productIds}` ); this.handleUpdateProductDetailsEvent(event); }); this.eventEmitter.addListener(FWEventName.LogMessage, () => {}); this.eventEmitter.addListener( FWEventName.CustomLinkButtonClick, (event) => { FWLoggerUtil.log( `Receive CustomLinkButtonClick event url: ${event?.url}` ); this.handleCustomLinkButtonClickEvent(event); } ); this.eventEmitter.addListener(FWEventName.CustomProductCardTap, (event) => { FWLoggerUtil.log(`Receive CustomProductCardTap event url: ${event?.url}`); if (event) { if (event.callbackId) { const callbackId = event.callbackId; delete event.callbackId; if (Platform.OS === 'ios') { event.playerHandler = { pause: () => ShoppingModule.pausePlayer(callbackId), resume: () => ShoppingModule.resumePlayer(callbackId), }; } } this.handleCustomProductCardTapEvent(event); } }); this.eventEmitter.addListener(FWEventName.ProductClick, (event) => { FWLoggerUtil.log(`Receive ProductClick event url: ${event?.url}`); if (event) { this.handleProductClickEvent(event); } }); } /** * * @param {number} count The number of items in the host app cart. * The count should be greater than or equal to 0. * We just use count to show or hide red indicator on the cart icon. * If cound > 0, we will show the red indicator on the cart icon. * Otherwise, we will hide the red indicator on the cart icon. */ public async setCartItemCount(count: number): Promise { if (!FWGlobalState.getInstance().sdkInitSucceeded) { await FWGlobalState.getInstance().sdkInitSucceededPromise; } ShoppingModule.setCartItemCount(count); } private async handleShoppingCTAEvent(event: any) { const callbackId = event.callbackId; delete event.callbackId; if (this.onShoppingCTA) { const result = await this.onShoppingCTA(event as ShoppingCTAEvent); if (callbackId) { ShoppingModule.updateShoppingCTAResult(result, callbackId); } } else { if (callbackId) { if (Platform.OS === 'ios') { ShoppingModule.clearCallbackId( callbackId, FWEventName.ShoppingCTAButtonClick ); } } } } private async handleClickCartIconEvent(event: any) { if (this.onCustomClickCartIcon) { this.onCustomClickCartIcon(event as CustomClickCartIconEvent); } } private async handleUpdateProductDetailsEvent(event: any) { const callbackId = event.callbackId; delete event.callbackId; if (this.onUpdateProductDetails) { FWLoggerUtil.log( `Call onUpdateProductDetails callback productIds: ${event.productIds} callbackId: ${callbackId}` ); try { const productList = await this.onUpdateProductDetails( event as UpdateProductDetailsEvent ); const productIds = (productList || []).map( (product) => product.productId ?? '' ); FWLoggerUtil.log( `Get result from onUpdateProductDetails callback productIds: ${productIds} productListLength: ${ (productList || []).length }` ); if (productList) { if (callbackId) { ShoppingModule.updateVideoProducts(productList, callbackId); } } else { if (callbackId) { if (Platform.OS === 'ios') { ShoppingModule.clearCallbackId( callbackId, FWEventName.UpdateProductDetails ); } } } } catch (error) { FWLoggerUtil.log( `onUpdateProductDetails callback threw an error callbackId: ${callbackId} error: ${ error instanceof Error ? (error.stack ?? error.message) : String(error) }` ); if (callbackId) { if (Platform.OS === 'ios') { ShoppingModule.clearCallbackId( callbackId, FWEventName.UpdateProductDetails ); } } } } else { FWLoggerUtil.log('onUpdateProductDetails callback is not set.'); if (callbackId) { if (Platform.OS === 'ios') { ShoppingModule.clearCallbackId( callbackId, FWEventName.UpdateProductDetails ); } } } } private async handleCustomLinkButtonClickEvent( event: CustomClickLinkButtonEvent ) { if (this.onCustomClickLinkButton) { this.onCustomClickLinkButton(event); } } private async handleCustomProductCardTapEvent( event: CustomTapProductCardEvent ) { if (this.onCustomTapProductCard) { this.onCustomTapProductCard(event); } } private async handleProductClickEvent(event: ClickProductEvent) { if (this.onClickProduct) { this.onClickProduct(event); } } } export default VideoShopping;