From d5b645d454918f43b06896a80a56b3bd0e2ecd8c Mon Sep 17 00:00:00 2001 From: Heihokon Date: Wed, 25 Mar 2026 08:36:31 -0500 Subject: [PATCH] feat: Add QA Assistant integration for mobile (React Native) with event bus, message handling, and cleanup logic Support forced variation allocation/unallocation in QA mode Add visitorVariationState initialization for both QA and non-QA modes Refactor BucketingManager targeting/operator evaluation logic Enhance ApiManager and campaign data structures (add campaign name) Add unit tests for mobile QA assistant modules Update version to 5.2.0 and fix README branding/links --- README.md | 39 +- package.json | 10 +- src/FlagshipProvider.tsx | 19 +- src/index.ts | 4 + src/qaAssistant/ABTastyQAContext.ts | 6 + src/qaAssistant/ABTastyQAProvider.tsx | 30 ++ src/qaAssistant/hooks.ts | 6 + src/sdkVersion.ts | 2 +- src/type.ts | 9 +- test/TouchCaptureProvider.test.tsx | 498 +++++++++++++++++--- test/qaAssistant/ABTastyQAProvider.test.tsx | 157 ++++++ test/qaAssistant/hooks.test.tsx | 83 ++++ yarn.lock | 26 +- 13 files changed, 787 insertions(+), 102 deletions(-) create mode 100644 src/qaAssistant/ABTastyQAContext.ts create mode 100644 src/qaAssistant/ABTastyQAProvider.tsx create mode 100644 src/qaAssistant/hooks.ts create mode 100644 test/qaAssistant/ABTastyQAProvider.test.tsx create mode 100644 test/qaAssistant/hooks.test.tsx diff --git a/README.md b/README.md index d2b23aa2..934fb2b0 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,26 @@ +[![Test](https://github.com/flagship-io/flagship-ts-sdk/actions/workflows/ci_push.yml/badge.svg)](https://github.com/flagship-io/flagship-ts-sdk/actions/workflows/ci_push.yml) [![codecov](https://codecov.io/gh/flagship-io/flagship-ts-sdk/branch/main/graph/badge.svg?token=IW0NWPTPSH)](https://codecov.io/gh/flagship-io/flagship-ts-sdk) [![npm version](https://badge.fury.io/js/@flagship.io%2Fjs-sdk.svg)](https://badge.fury.io/js/@flagship.io%2Fjs-sdk) + ## About Flagship ​ -drawing +drawing ​ -[Flagship by AB Tasty](https://www.flagship.io/) is a feature flagging platform for modern engineering and product teams. It eliminates the risks of future releases by separating code deployments from these releases :bulb: With Flagship, you have full control over the release process. You can: +[Flagship by AB Tasty](https://www.abtasty.com) is a feature flagging platform for modern engineering and product teams. It eliminates the risks of future releases by separating code deployments from these releases :bulb: With Flagship, you have full control over the release process. You can: ​ -- Switch features on or off through remote config. -- Automatically roll-out your features gradually to monitor performance and gather feedback from your most relevant users. -- Roll back any feature should any issues arise while testing in production. -- Segment users by granting access to a feature based on certain user attributes. -- Carry out A/B tests by easily assigning feature variations to groups of users. - ​ - drawing - ​ - Flagship also allows you to choose whatever implementation method works for you from our many available SDKs or directly through a REST API. Additionally, our architecture is based on multi-cloud providers that offer high performance and highly-scalable managed services. - ​ - **To learn more:** - ​ -- [Solution overview](https://www.flagship.io/#showvideo) - A 5mn video demo :movie_camera: -- [Documentation](https://docs.abtasty.com/server-side/sdks/react-native) - Our dev portal with guides, how tos, API and SDK references -- [Sign up for a free trial](https://www.flagship.io/sign-up/) - Create your free account -- [Guide to feature flagging](https://www.flagship.io/feature-flags/) - Everyhting you need to know about feature flag related use cases -- [Blog](https://www.flagship.io/blog/) - Additional resources about release management +- Switch features on or off through remote config. +- Automatically roll-out your features gradually to monitor performance and gather feedback from your most relevant users. +- Roll back any feature should any issues arise while testing in production. +- Segment users by granting access to a feature based on certain user attributes. +- Carry out A/B tests by easily assigning feature variations to groups of users. + ​ + ​ + Flagship also allows you to choose whatever implementation method works for you from our many available SDKs or directly through a REST API. Additionally, our architecture is based on multi-cloud providers that offer high performance and highly-scalable managed services. + ​ + **To learn more:** + ​ +- [Solution overview](https://www.abtasty.com/feature-experimentation/) - Discover how Flagship can help you manage your releases and run experiments in production +- [Documentation](https://docs.abtasty.com/server-side/sdks/sdk-overview) - Our dev portal with guides, how tos, API and SDK references +- [Sign up for a free trial](https://www.abtasty.com/get-a-demo/) - Try out Flagship for free and see how it can help you manage your releases and run experiments in production +- [Guide to feature flagging](https://docs.abtasty.com/feature-experimentation-and-rollout) - Everyhting you need to know about feature flag related use cases +- [Blog](https://www.abtasty.com/resources/) - Additional resources about release management diff --git a/package.json b/package.json index 36fae428..217df29c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@flagship.io/react-native-sdk", - "version": "5.0.3", + "version": "5.1.0", "description": "Flagship SDK for React Native", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -22,9 +22,6 @@ "type": "git", "url": "git+https://github.com/flagship-io/flagship-react-native-sdk.git" }, - "dependencies": { - "@flagship.io/react-sdk": "^5.2.2" - }, "peerDependencies": { "@react-native-async-storage/async-storage": ">=1.17.0", "react-native": ">=0.60.0" @@ -75,5 +72,8 @@ "ts-jest": "^29.1.0", "typescript": "^5.0.4" }, - "packageManager": "yarn@4.9.0+sha512.5f5f00843eae735dfab6f0442524603e987ceca55e98b36eb0120f4e58908e5b1406545321e46627dca97d15d562f23dc13fb96cabd4e6bc92d379f619001e4e" + "packageManager": "yarn@4.9.0+sha512.5f5f00843eae735dfab6f0442524603e987ceca55e98b36eb0120f4e58908e5b1406545321e46627dca97d15d562f23dc13fb96cabd4e6bc92d379f619001e4e", + "dependencies": { + "@flagship.io/react-sdk": "^5.2.3" + } } diff --git a/src/FlagshipProvider.tsx b/src/FlagshipProvider.tsx index 7bbda2c7..0542e6c5 100644 --- a/src/FlagshipProvider.tsx +++ b/src/FlagshipProvider.tsx @@ -14,7 +14,13 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { Platform } from 'react-native'; import { version as SDK_VERSION } from './sdkVersion'; import { TouchCaptureProvider } from './TouchCaptureProvider'; -import { CLIENT_CACHE_KEY, DEFAULT_POOL_MAX_SIZE, DEFAULT_TIME_INTERVAL, SDK_FIRST_TIME_INIT } from './Constant'; +import { + CLIENT_CACHE_KEY, + DEFAULT_POOL_MAX_SIZE, + DEFAULT_TIME_INTERVAL, + SDK_FIRST_TIME_INIT +} from './Constant'; +import { ABTastyQAProvider } from './qaAssistant/ABTastyQAProvider'; export interface FlagshipProviderProps extends Omit< @@ -121,9 +127,12 @@ const FlagshipProviderFunc = ({ ); }, [props.trackingManagerConfig]); + const isQAModeEnabled = props.isQAModeEnabled || false; + return ( - {children} + + {children} + ); }; diff --git a/src/index.ts b/src/index.ts index 43ba1c94..b5886494 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,10 @@ export { FlagshipProvider } from './FlagshipProvider' export type { FlagshipProviderProps } from './FlagshipProvider' +export { useABTastyQA } from './qaAssistant/hooks' + +export { ABTastyQA } from './type' + export { useFlagship, UseFlagshipOutput } from './FlagshipHooks' diff --git a/src/qaAssistant/ABTastyQAContext.ts b/src/qaAssistant/ABTastyQAContext.ts new file mode 100644 index 00000000..eda0ac3e --- /dev/null +++ b/src/qaAssistant/ABTastyQAContext.ts @@ -0,0 +1,6 @@ + +import { createContext } from "react"; +import type { ABTastyQA } from "../type"; + + +export const ABTastyQAContext = createContext(null); \ No newline at end of file diff --git a/src/qaAssistant/ABTastyQAProvider.tsx b/src/qaAssistant/ABTastyQAProvider.tsx new file mode 100644 index 00000000..d64f78bd --- /dev/null +++ b/src/qaAssistant/ABTastyQAProvider.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { ABTastyQAEventBus } from '@flagship.io/react-sdk'; + +import { useMemo } from 'react'; +import { ABTastyQAContext } from './ABTastyQAContext'; + +type ABTastyQAProviderProps = { + children: React.ReactNode; + isQAModeEnabled?: boolean; + envId?: string; + apiKey?: string; +}; + +export function ABTastyQAProvider({ + children, + isQAModeEnabled, + envId, + apiKey +}: ABTastyQAProviderProps) { + const ProviderValue = useMemo( + () => ({ ABTastyQAEventBus, isQAModeEnabled, envId, apiKey }), + [isQAModeEnabled, envId, apiKey] + ); + + return ( + + {children} + + ); +} diff --git a/src/qaAssistant/hooks.ts b/src/qaAssistant/hooks.ts new file mode 100644 index 00000000..86dc721f --- /dev/null +++ b/src/qaAssistant/hooks.ts @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { ABTastyQAContext } from "./ABTastyQAContext"; + +export function useABTastyQA() { + return useContext(ABTastyQAContext); +} \ No newline at end of file diff --git a/src/sdkVersion.ts b/src/sdkVersion.ts index 1e2ca71a..adac1e96 100644 --- a/src/sdkVersion.ts +++ b/src/sdkVersion.ts @@ -1,2 +1,2 @@ // Generated by genversion. -export const version = '5.0.3' +export const version = '5.1.0' diff --git a/src/type.ts b/src/type.ts index 7840f9d7..c92c4409 100644 --- a/src/type.ts +++ b/src/type.ts @@ -1,6 +1,13 @@ -import { IPageView, IVisitorEvent, Visitor } from "@flagship.io/react-sdk"; +import { IPageView, IVisitorEvent, Visitor, ABTastyQAEventBus } from "@flagship.io/react-sdk"; export type VisitorAugmented = typeof Visitor & { sendEaiPageView: (pageView: IPageView) => void; sendEaiVisitorEvent: (visitorEvent: IVisitorEvent) => void; +} + +export interface ABTastyQA { + ABTastyQAEventBus: typeof ABTastyQAEventBus; + isQAModeEnabled?: boolean; + envId?: string; + apiKey?: string; } \ No newline at end of file diff --git a/test/TouchCaptureProvider.test.tsx b/test/TouchCaptureProvider.test.tsx index a0b05e31..f08fbc59 100644 --- a/test/TouchCaptureProvider.test.tsx +++ b/test/TouchCaptureProvider.test.tsx @@ -4,8 +4,7 @@ import { render, fireEvent, act, - waitFor, - userEvent + waitFor } from '@testing-library/react-native'; import { TouchCaptureProvider } from '../src/TouchCaptureProvider'; import { Flagship, IVisitorEvent } from '@flagship.io/react-sdk'; @@ -23,20 +22,18 @@ jest.mock('@flagship.io/react-sdk', () => { }); function sleep(time: number): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { setTimeout(() => { resolve(); }, time); }); } -describe('TouchCaptureProvider', () => { +describe('TouchCaptureProvider - Basic Rendering', () => { const mockSendEaiVisitorEvent = jest.fn(); const mockOnEAICollectStatusChange = jest.fn<(fn: (status: boolean) => void) => void>(); - const fixedTimestamp = 254889889; // 1978-05-25T11:48:09.889Z - beforeEach(() => { jest.clearAllMocks(); const mockVisitor = { @@ -55,11 +52,7 @@ describe('TouchCaptureProvider', () => { }); }); - afterEach(() => { - jest.useRealTimers(); - }); - - it('renders children correctly', () => { + it('should render children correctly within the provider', () => { const { getByText } = render( @@ -70,7 +63,72 @@ describe('TouchCaptureProvider', () => { expect(getByText('Child Component')).toBeTruthy(); }); - it('calls sendTouchPositionEvent on touch start', async () => { + it('should not register EAI callback when visitor is undefined', async () => { + (Flagship.getVisitor as jest.Mock).mockReturnValue(undefined); + + render( + + + Test + + + ); + + await waitFor(() => { + expect(mockOnEAICollectStatusChange).not.toHaveBeenCalled(); + }); + }); + + it('should not register EAI callback when onEAICollectStatusChange is not available', async () => { + const mockVisitor = { + visitorId: 'testVisitorId', + sendEaiVisitorEvent: mockSendEaiVisitorEvent + // onEAICollectStatusChange is missing + }; + (Flagship.getVisitor as jest.Mock).mockReturnValue(mockVisitor); + + render( + + + Test + + + ); + + await waitFor(() => { + expect(mockOnEAICollectStatusChange).not.toHaveBeenCalled(); + }); + }); +}); + +describe('TouchCaptureProvider - Touch Position Events', () => { + const mockSendEaiVisitorEvent = jest.fn(); + const mockOnEAICollectStatusChange = + jest.fn<(fn: (status: boolean) => void) => void>(); + + beforeEach(() => { + jest.clearAllMocks(); + const mockVisitor = { + visitorId: 'testVisitorId', + sendEaiVisitorEvent: mockSendEaiVisitorEvent, + onEAICollectStatusChange: mockOnEAICollectStatusChange + }; + (Flagship.getVisitor as jest.Mock).mockReturnValue(mockVisitor); + Dimensions.get = jest + .fn<(dim: 'window' | 'screen') => ScaledSize>() + .mockReturnValue({ + width: 1080, + height: 1920, + scale: 1, + fontScale: 1 + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should send touch position event after timeout when touch starts', async () => { let onEAICollectStatusChange: ((status: boolean) => void) | undefined; mockOnEAICollectStatusChange.mockImplementation((fn) => { onEAICollectStatusChange = fn; @@ -105,38 +163,63 @@ describe('TouchCaptureProvider', () => { }, { timeout: TIMEOUT_DURATION + 100 } ); + }); + it('should send previous touch position immediately when new touch starts', async () => { + let onEAICollectStatusChange: ((status: boolean) => void) | undefined; + mockOnEAICollectStatusChange.mockImplementation((fn) => { + onEAICollectStatusChange = fn; + }); + + const { getByTestId } = render( + +