Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@absmartly/react-sdk",
"version": "1.4.0",
"version": "1.4.2-beta.1",
"homepage": "https://github.com/absmartly/react-sdk#README.md",
"bugs": "https://github.com/absmartly/react-sdk/issues",
"keywords": [
Expand Down
36 changes: 28 additions & 8 deletions src/components/SDKProvider/SDKProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, type FC, type ReactNode } from "react";

import absmartly from "@absmartly/javascript-sdk";
import absmartly, { Context, SDK } from "@absmartly/javascript-sdk";

import { _SdkContext } from "../../hooks/useABSmartly";
import type {
Expand All @@ -25,31 +25,51 @@ type SDKProviderWithContext = {
contextOptions?: never;
};

type SDKProviderProps = SDKProviderNoContext | SDKProviderWithContext;
type SDKProviderNullContext = {
context: null;
children?: ReactNode;
sdkOptions: SDKOptionsType;
contextOptions?: never;
};

type SDKProviderProps =
| SDKProviderNoContext
| SDKProviderWithContext
| SDKProviderNullContext;

export const SDKProvider: FC<SDKProviderProps> = ({
sdkOptions,
contextOptions,
context,
children,
}) => {
const sdk = context
? context["_sdk"]
: new absmartly.SDK({ retries: 5, timeout: 3000, ...sdkOptions });
const sdk: SDK =
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Question]: Wouldn't it be better to also keep the SDK null until a non-null context is passed (or requested via resetContext)?

context == null
? new absmartly.SDK({ retries: 5, timeout: 3000, ...sdkOptions })
: context["_sdk"];

const [providedContext, setProvidedContext] = useState(
context ? context : sdk.createContext(contextOptions),
const [providedContext, setProvidedContext] = useState<Context | null>(
context === null
? null
: context === undefined
? sdk.createContext(contextOptions)
: context,
);

const resetContext = async (
params: ContextRequestType,
contextOptions?: ContextOptionsType,
) => {
try {
if (providedContext == null) {
Copy link

@caal-15 caal-15 Oct 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion]: I think this implies the only method of replacing the null context is via the resetContext function, while this technically works it could create a disconnect between the context being passed to the provider (i.e. null) and the internal context of the provider (an actual fully functional context).

I think a better approach would be to have an useEffect hook listening to possible changes in the context, so that when the context passed to the provider is no longer null we update the internal values, here's a snippet of how it could potentially work:

export const SDKProvider: FC<SDKProviderProps> = ({
  sdkOptions,
  contextOptions,
  context,
  children,
}) => {
  let contextInitialValue = context;
  let sdkInitialValue = context ? context['_sdk'] : null;
  if (context === undefined) {
    sdkInitialValue = new absmartly.SDK({ retries: 5, timeout: 3000, ...sdkOptions });
    contextInitialValue = sdk.createContext(contextOptions);
  }

  const [context, setContext] = useState<Context | null>(contextInitialValue);
  const [sdk, setSdk] = useState<SDK | null>(sdkInitialValue);

  useEffect(() => {
    if (!!context) {
      setContext(context);
      setSdk(context['_sdk']);
    };   
  }, [context]);
};

You could probably improve performance a bit by memoizing values and such, but this is the gist of it :)

setProvidedContext(sdk.createContext(params, contextOptions));
return;
}

await providedContext.ready();

const contextData = providedContext.data();
const oldContextOptions = providedContext._opts;
const oldContextOptions = providedContext["_opts"];

const combinedContextOptions = {
...oldContextOptions,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Treatment/Treatment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const Treatment: FC<TreatmentProps> = ({
const ensuredContext = context ?? useABSmartly().context;

const [loading, setLoading] = useState<boolean>(
ensuredContext && !ensuredContext.isReady(),
ensuredContext != null ? !ensuredContext.isReady() : true,
);

// Turning the children into an array of objects and mapping them as variants
Expand Down
6 changes: 5 additions & 1 deletion src/components/Treatment/TreatmentFunction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const TreatmentFunction: FC<TreatmentFunctionProps> = ({
variables: {},
});

const [loading, setLoading] = useState<boolean>(!ensuredContext.isReady());
const [loading, setLoading] = useState<boolean>(!ensuredContext?.isReady());

const getLoadingComponent = () => {
return loadingComponent != null ? (
Expand All @@ -42,6 +42,10 @@ export const TreatmentFunction: FC<TreatmentFunctionProps> = ({
);
};

if (ensuredContext === null) {
return getLoadingComponent();
}

// Set variant number and variables in state
useEffect(() => {
if (attributes) ensuredContext.attributes(attributes);
Expand Down
4 changes: 4 additions & 0 deletions src/hooks/useTreatment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export const useTreatment = (name: string, peek = false) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

if (context === null) {
return { variant, loading, error, context };
}

useEffect(() => {
const fetchTreatment = async () => {
try {
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type ABSmartlySDK = SDK;

export type ABSmartly = {
sdk: ABSmartlySDK;
context: ABSmartlyContext;
context: ABSmartlyContext | null;
resetContext: (
contextRequest: ContextRequestType,
contextOptions?: ContextOptionsType,
Expand Down
72 changes: 61 additions & 11 deletions tests/SDKProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
act,
cleanup,
fireEvent,
render,
renderHook,
screen,
waitFor,
} from "@testing-library/react";
import { FC, PropsWithChildren } from "react";
import { afterEach, describe, expect, it, MockedClass, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { useEffect, useState, type FC, type PropsWithChildren } from "react";
import { afterEach, describe, expect, it, vi, type MockedClass } from "vitest";

import { Context, SDK } from "@absmartly/javascript-sdk";

Expand Down Expand Up @@ -93,7 +93,7 @@ describe("SDKProvider", () => {
},
};

it("Whether it creates an instance of the ABSmartly JS-SDK and an ABSmartly Context", async () => {
it("Whether it creates an instance of the ABSmartly JS-SDK and an ABSmartly Context", () => {
render(
<SDKProvider sdkOptions={sdkOptions} contextOptions={contextOptions}>
<TestComponent />
Expand All @@ -107,7 +107,7 @@ describe("SDKProvider", () => {
expect(mockCreateContext).toHaveBeenLastCalledWith(contextOptions);
});

it("Whether it will create an SDK instance with a context that has prefetched context data", async () => {
it("Whether it will create an SDK instance with a context that has prefetched context data", () => {
render(
<SDKProvider context={mockContext}>
<TestComponent />
Expand All @@ -118,13 +118,65 @@ describe("SDKProvider", () => {
expect(mockCreateContext).not.toHaveBeenCalled();
});

it("Whether useABSmartly throws an error when not used within an SDKProvider", async () => {
it("Works when the context is passed in later, when available", async () => {
const useFakeHook = () => {
const [userId, setUserId] = useState<string | null>(null);
const [anonymousId, setAnonymousId] = useState<string | null>(null);

useEffect(() => {
const timer = setTimeout(async () => {
setAnonymousId("test-anonymous-id");
setUserId("test-user-id");
}, 0);

return () => clearTimeout(timer);
}, []);
return {
userId,
anonymousId,
};
};

const wrapper: FC<PropsWithChildren> = ({ children }) => (
<SDKProvider sdkOptions={sdkOptions} context={null}>
{children}
</SDKProvider>
);

const { result: hookResult } = renderHook(() => useFakeHook(), { wrapper });
const { result } = renderHook(() => useABSmartly(), { wrapper });

expect(hookResult.current.userId).toBe(null);
expect(hookResult.current.anonymousId).toBe(null);

expect(result.current.sdk).toBeDefined();
expect(result.current.context).toBeNull();

const newContextOptions = {
units: {
userId: hookResult.current.userId ?? "",
anonymousId: hookResult.current.anonymousId ?? "",
},
};

await result.current.resetContext(newContextOptions);

await waitFor(async () => {
expect(hookResult.current.userId).toBe("test-user-id");
expect(hookResult.current.anonymousId).toBe("test-anonymous-id");

expect(result.current.context).toBeDefined();
expect(result.current.context).not.toBeNull();
});
});

it("Whether useABSmartly throws an error when not used within an SDKProvider", () => {
expect(() => renderHook(() => useABSmartly())).toThrow(
"useABSmartly must be used within an SDKProvider. https://docs.absmartly.com/docs/SDK-Documentation/getting-started#import-and-initialize-the-sdk",
);
});

it("Whether useABSmartly hook works", async () => {
it("Whether useABSmartly hook works", () => {
const wrapper: FC<PropsWithChildren> = ({ children }) => (
<SDKProvider sdkOptions={sdkOptions} contextOptions={contextOptions}>
{children}
Expand Down Expand Up @@ -162,9 +214,7 @@ describe("SDKProvider", () => {
);

const button = screen.getByText("Reset Context");
await act(async () => {
fireEvent.click(button);
});
await userEvent.click(button);

expect(mockCreateContextWith).toHaveBeenCalledTimes(1);
expect(mockCreateContextWith).toHaveBeenCalledWith(
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@
"declarationMap": true
},
"include": ["src", "tests"],
"exclude": ["node_modules", "lib"],
"exclude": ["node_modules", "lib", "tests"],
"typeRoots": ["node_modules/@types", "src/**/*.d.ts"]
}
Loading