diff --git a/src/TaplyticsProvider/TaplyticsProvider.hooks.ts b/src/TaplyticsProvider/TaplyticsProvider.hooks.ts new file mode 100644 index 0000000..5413217 --- /dev/null +++ b/src/TaplyticsProvider/TaplyticsProvider.hooks.ts @@ -0,0 +1,105 @@ +import { useEffect, useRef } from 'react' +import { AppState, AppStateStatus, EventSubscription, Platform } from 'react-native' + +import { getRunningFeatureFlags, getRunningExperimentsAndVariations } from '../experiments' +import { setTaplyticsNewSessionListener } from '../user' + +import { TaplyticsProviderHooksArgs } from './TaplyticsProvider.types' + +/** + * Helper function that returns a promise that resolves in an array of running feature flags, + * and experiments and variations. + */ +const getProperties = async () => Promise.all([getRunningFeatureFlags(), getRunningExperimentsAndVariations()]) + +/** + * @description This hook utilizes the `setTaplyticsNewSessionListener` listener to detect + * when a new session has been initiated and updates the context state. The listener also + * gets triggered when the `user_id` or `email` attribute of the user is updated through the + * `setUserAttributes` method. + * + * On `android` the `setTaplyticsNewSessionListener` is not triggered upon app start. + * The `getRunningExperimentsAndVariations` method is used instead. + * + * @param setStateFunctions An object of type `TaplyticsProviderHooksArgs` which contains + * functions to manipulate the context state. + */ +export const useAppStartAndNewSessionListener = ({ + setError, + setIsLoading, + setRunningFeatureFlags, + setExperiments, +}: TaplyticsProviderHooksArgs): void => { + useEffect(() => { + let subscriber: EventSubscription | undefined + + try { + setIsLoading(true) + + // If on Android, fetch properties on App Start + if (Platform.OS === 'android') { + ;(async () => { + const [runningFeatureFlags, experiments] = await getProperties() + setRunningFeatureFlags(runningFeatureFlags) + setExperiments(experiments) + + setIsLoading(false) + })() + } + + subscriber = setTaplyticsNewSessionListener(async () => { + const [runningFeatureFlags, experiments] = await getProperties() + + setRunningFeatureFlags(runningFeatureFlags) + setExperiments(experiments) + + setIsLoading(false) + }) + } catch (error) { + setIsLoading(false) + setError(error) + } + + return () => { + subscriber?.remove && subscriber.remove() + } + }, []) +} + +/** + * @description This hook utilizes the react-native `AppState` listener to detect + * when the app goes from background to foreground and updates the context state. + * + * @param setStateFunctions An object of type `TaplyticsProviderHooksArgs` which contains + * functions to manipulate the context state. + */ +export const useAppStateListener = ({ setError, setIsLoading, setRunningFeatureFlags, setExperiments }: TaplyticsProviderHooksArgs) => { + const appState = useRef(AppState.currentState) + + const handleAppStateChange = async (nextAppState: AppStateStatus) => { + if (appState.current === 'background' && nextAppState === 'active') { + try { + setIsLoading(true) + + const [runningFeatureFlags, experiments] = await getProperties() + + setRunningFeatureFlags(runningFeatureFlags) + setExperiments(experiments) + + setIsLoading(false) + } catch (error) { + setIsLoading(false) + setError(error) + } + } + appState.current = nextAppState + } + + useEffect(() => { + AppState.addEventListener('change', handleAppStateChange) + + return () => { + AppState.removeEventListener('change', handleAppStateChange) + } + }, []) +} diff --git a/src/TaplyticsProvider/TaplyticsProvider.tsx b/src/TaplyticsProvider/TaplyticsProvider.tsx new file mode 100644 index 0000000..ab179f3 --- /dev/null +++ b/src/TaplyticsProvider/TaplyticsProvider.tsx @@ -0,0 +1,51 @@ +import React, { FC, createContext, useState } from 'react' + +import { TaplyticsFeatureFlags, TaplyticsExperiments } from '../experiments' + +import { useAppStateListener, useAppStartAndNewSessionListener } from './TaplyticsProvider.hooks' +import { ITaplyticsContext, TaplyticsProviderHooksArgs } from './TaplyticsProvider.types' + +export const TaplyticsContext = createContext({ + loading: false, + error: null, + runningFeatureFlags: {}, + experiments: {}, +}) + +/** + * A provider component that is used to wrap an application/component. This allows you to be able to + * utilize hooks provided by Taplytics. + * + */ +const TaplyticsProvider: FC = ({ children }) => { + const [loading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const [runningFeatureFlags, setRunningFeatureFlags] = useState({}) + const [experiments, setExperiments] = useState({}) + + const hooksArgs: TaplyticsProviderHooksArgs = { + setError, + setIsLoading, + setRunningFeatureFlags, + setExperiments, + } + + useAppStartAndNewSessionListener(hooksArgs) + useAppStateListener(hooksArgs) + + return ( + + {children} + + ) +} + +export default TaplyticsProvider diff --git a/src/TaplyticsProvider/TaplyticsProvider.types.ts b/src/TaplyticsProvider/TaplyticsProvider.types.ts new file mode 100644 index 0000000..1ee8619 --- /dev/null +++ b/src/TaplyticsProvider/TaplyticsProvider.types.ts @@ -0,0 +1,20 @@ +import { TaplyticsFeatureFlags, TaplyticsExperiments } from '../experiments' + +export interface ITaplyticsContext { + loading: boolean + error: Error | null + runningFeatureFlags: TaplyticsFeatureFlags + experiments: TaplyticsExperiments +} + +export type TaplyticsHookMetaData = { + loading: boolean + error: Error | null +} + +export type TaplyticsProviderHooksArgs = { + setIsLoading: (isLoading: boolean) => void + setError: (error: Error) => void + setRunningFeatureFlags: (runningFeatureFlags: TaplyticsFeatureFlags) => void + setExperiments: (experiments: TaplyticsExperiments) => void +} diff --git a/src/TaplyticsProvider/index.ts b/src/TaplyticsProvider/index.ts new file mode 100644 index 0000000..ae8fb12 --- /dev/null +++ b/src/TaplyticsProvider/index.ts @@ -0,0 +1,5 @@ +import TaplyticsProvider, { TaplyticsContext } from './TaplyticsProvider' +import { TaplyticsHookMetaData, ITaplyticsContext, TaplyticsProviderHooksArgs } from './TaplyticsProvider.types' + +export { TaplyticsContext, TaplyticsHookMetaData, ITaplyticsContext, TaplyticsProviderHooksArgs } +export default TaplyticsProvider diff --git a/src/experiments/experiments.ts b/src/experiments/experiments.ts index cb78d61..c4f91c9 100644 --- a/src/experiments/experiments.ts +++ b/src/experiments/experiments.ts @@ -20,8 +20,7 @@ const TaplyticsEventEmitter = new NativeEventEmitter(Taplytics) export const runCodeBlock = (name: string, codeBlock: CodeBlockCallback): void => Taplytics._runCodeBlock(name, codeBlock) /** - * @deprecated Use the `setTaplyticsNewSessionListner` instead for a more consistent behaviour - * across `android` and `ios` devices. + * @deprecated Use the `setTaplyticsNewSessionListner` instead. * * Use this method to ensure that all the feature flag and experiment variables * have been loaded from the server prior to utlizing them. The callback @@ -123,14 +122,6 @@ const updateDynamicVariables = (name: string, value: string | boolean | object | */ export const getVariables = (): TaplyticsVariableMap => cloneDeep(dynamicVariables) -/** - * A Map of callbacks used by the `newAsyncVariable` funciton to keep track of - * which callback to trigger when the `asyncVariable` event is called. - * The Map key is an ID that is incremented dependant on the number of `newAsyncVariable` - * function invokations. - */ -const asyncVariableCallbackMap = new Map void>() - /** * A ID that is used to track each invokation of the `newAsyncVariable` method. * This ID is passed to the native layer, and eventually passed back whenever the diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..ed7088d --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,3 @@ +import useFeatureFlag from './useFeatureFlag' + +export { useFeatureFlag } diff --git a/src/hooks/useFeatureFlag.ts b/src/hooks/useFeatureFlag.ts new file mode 100644 index 0000000..61b9f5d --- /dev/null +++ b/src/hooks/useFeatureFlag.ts @@ -0,0 +1,32 @@ +import { useContext } from 'react' +import { TaplyticsFeatureFlags } from '../experiments' +import { TaplyticsContext, TaplyticsHookMetaData } from '../TaplyticsProvider' + +/** + * Returns all available feature flags, and a meta data object. + * + */ +function useFeatureFlag(): [TaplyticsFeatureFlags, TaplyticsHookMetaData] + +/** + * Returns a boolean indicating if the feature flag is turned on, and a meta data object. + * + * @param name The name of the feature flag + * + */ +function useFeatureFlag(name: string): [boolean, TaplyticsHookMetaData] + +function useFeatureFlag(name?: any) { + const { runningFeatureFlags, loading, error } = useContext(TaplyticsContext) + + const metaData: TaplyticsHookMetaData = { loading, error } + + if (name === undefined) return [runningFeatureFlags, metaData] + + const featureFlags = Object.values(runningFeatureFlags) + const isFeatureFlagActive = featureFlags.includes(name) + + return [!!isFeatureFlagActive, metaData] +} + +export default useFeatureFlag diff --git a/src/index.ts b/src/index.ts index 02172ba..2e1785a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,8 @@ import { registerPushOpenedListener, registerPushReceivedListener, } from './push' +import { useFeatureFlag } from './hooks' +import TaplyticsProvider from './TaplyticsProvider' export { TaplyticsAndroidNotification, @@ -31,6 +33,7 @@ export { TaplyticsUserAttributes, TaplyticsVariable, TaplyticsVariableMap, + TaplyticsProvider, featureFlagEnabled, getRunningExperimentsAndVariations, getRunningFeatureFlags, @@ -49,4 +52,5 @@ export { setTaplyticsNewSessionListener, setUserAttributes, startNewSession, + useFeatureFlag, } diff --git a/src/user/user.ts b/src/user/user.ts index ccf3eb3..4f09808 100644 --- a/src/user/user.ts +++ b/src/user/user.ts @@ -20,11 +20,19 @@ export const getSessionInfo = (): Promise => Taplytics._ge /** * Use this listener to ensure that all the properties have been loaded from the server - * prior to utlizing them. The listener returns back an event subscriber that can be used to - * cleanup the event listener using the `remove` function. + * prior to utlizing feature flags and experiment variable values. The listener returns + * back an event subscriber that can be used to cleanup the event listener using the `remove` function. * - * The listener triggers when properties have been loaded, a new session is generated or a user's - * `email` or `user_id` fields are updating using the `setUserAttribute` method. + * On iOS this listener is triggered in the following situations: + * - Properties have been loaded on app start. + * - A new session is generated by the SDK, or when explicitly started using the `startNewSession` method. + * - A user's `email` or `user_id` fields are updated using the `setUserAttribute` method. + * + * On Android this listener is triggered in the following situations: + * - A new session is generated by the SDK, or when explicitly started using the `startNewSession` method. + * - A user's `email` or `user_id` fields are updated using the `setUserAttribute` method. + * + * *NOTE:* This listener does not trigger on app start for `android` devices. * * @param callback A function that gets executed when a new session is created. *