Skip to content

Commit

Permalink
TAP-6059: Added TaplyticsProvider (#13)
Browse files Browse the repository at this point in the history
* Refactored SDK to be more modular

* Removed TaplyticsProvider imports

* Removed unused and undocumented native methods

* Added _newSessionCallback native method and exported it as part of `setTaplyticsNewSessionListener` method

* Changed newSyncObject to take in NSDictionary

* Added getVariables and registerVaraiblesChangedListener methods and removed lodash and only added lodash.clonedeep

* Add library to gitignore and prettierignore

* Added extra line on ignore files

* Added push method types and cleaned up the push methods

* Removed old index files and added push types to index

* Added TaplyticsProvider

* Removed console.log and exported TaplyticsProvider from index.ts

* Updated TaplyticsProvider to fetch properties on App Start for android

* Export TaplyticsProvider

* Added useFeatureFlag hook (#15)
  • Loading branch information
hamzahayat authored Mar 26, 2021
1 parent d2f04d5 commit 5c9b1d9
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 14 deletions.
105 changes: 105 additions & 0 deletions src/TaplyticsProvider/TaplyticsProvider.hooks.ts
Original file line number Diff line number Diff line change
@@ -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<AppStateStatus>(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)
}
}, [])
}
51 changes: 51 additions & 0 deletions src/TaplyticsProvider/TaplyticsProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<ITaplyticsContext>({
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<boolean>(false)
const [error, setError] = useState<Error | null>(null)

const [runningFeatureFlags, setRunningFeatureFlags] = useState<TaplyticsFeatureFlags>({})
const [experiments, setExperiments] = useState<TaplyticsExperiments>({})

const hooksArgs: TaplyticsProviderHooksArgs = {
setError,
setIsLoading,
setRunningFeatureFlags,
setExperiments,
}

useAppStartAndNewSessionListener(hooksArgs)
useAppStateListener(hooksArgs)

return (
<TaplyticsContext.Provider
value={{
loading,
error,
runningFeatureFlags,
experiments,
}}
>
{children}
</TaplyticsContext.Provider>
)
}

export default TaplyticsProvider
20 changes: 20 additions & 0 deletions src/TaplyticsProvider/TaplyticsProvider.types.ts
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 5 additions & 0 deletions src/TaplyticsProvider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import TaplyticsProvider, { TaplyticsContext } from './TaplyticsProvider'
import { TaplyticsHookMetaData, ITaplyticsContext, TaplyticsProviderHooksArgs } from './TaplyticsProvider.types'

export { TaplyticsContext, TaplyticsHookMetaData, ITaplyticsContext, TaplyticsProviderHooksArgs }
export default TaplyticsProvider
11 changes: 1 addition & 10 deletions src/experiments/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,14 +122,6 @@ const updateDynamicVariables = (name: string, value: string | boolean | object |
*/
export const getVariables = (): TaplyticsVariableMap => cloneDeep<TaplyticsVariableMap>(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<number, (variable: any) => 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
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import useFeatureFlag from './useFeatureFlag'

export { useFeatureFlag }
32 changes: 32 additions & 0 deletions src/hooks/useFeatureFlag.ts
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
registerPushOpenedListener,
registerPushReceivedListener,
} from './push'
import { useFeatureFlag } from './hooks'
import TaplyticsProvider from './TaplyticsProvider'

export {
TaplyticsAndroidNotification,
Expand All @@ -31,6 +33,7 @@ export {
TaplyticsUserAttributes,
TaplyticsVariable,
TaplyticsVariableMap,
TaplyticsProvider,
featureFlagEnabled,
getRunningExperimentsAndVariations,
getRunningFeatureFlags,
Expand All @@ -49,4 +52,5 @@ export {
setTaplyticsNewSessionListener,
setUserAttributes,
startNewSession,
useFeatureFlag,
}
16 changes: 12 additions & 4 deletions src/user/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ export const getSessionInfo = (): Promise<TaplyticsSessionInfo> => 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.
*
Expand Down

0 comments on commit 5c9b1d9

Please sign in to comment.