Skip to content
Open
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
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"react": "18.2.0",
"react-is": "^18.2.0",
"react-native": "0.71.6",
"react-native-mmkv-storage": "^0.9.1",
"react-native-svg": "^13.8.0",
"styled-components": "^5.3.9"
},
Expand Down
1 change: 1 addition & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export function App(): ReactElement {
onBackdropPress="continue"
motion="bounce"
onStop={onStopTour}
autoStart="always"
>
{({ start }) => (
<>
Expand Down
4 changes: 4 additions & 0 deletions package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
},
"dependencies": {
"fast-equals": "^5.0.1",
"object-hash": "^3.0.0",
"react-native-mmkv-storage": "^0.9.1",
Copy link
Member

Choose a reason for hiding this comment

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

Let's move this to devDependencies as it's an optional peer dependency 🙂

"react-native-responsive-dimensions": "^3.1.1",
"styled-components": "^5.3.9"
},
Expand All @@ -47,6 +49,7 @@
"@testing-library/react-native": "^11.5.4",
"@types/jest": "^29.4.1",
"@types/node": "^18.15.3",
"@types/object-hash": "^3.0.2",
"@types/react-test-renderer": "^18.0.0",
"@types/sinon": "^10.0.13",
"@types/styled-components": "^5.1.26",
Expand All @@ -73,6 +76,7 @@
"peerDependencies": {
"react": ">=16.8.0",
"react-native": ">=0.50.0",
"react-native-mmkv-storage": ">=0.9.1",
Copy link
Member

Choose a reason for hiding this comment

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

We need to add this one to peerDependenciesMeta and mark it as optional: true

"react-native-svg": ">=12.1.0"
},
"peerDependenciesMeta": {
Expand Down
5 changes: 5 additions & 0 deletions package/src/helpers/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { MMKVLoader } from "react-native-mmkv-storage";
Copy link
Member

Choose a reason for hiding this comment

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

This is an optional dependency, so it's possible that it's not present in some environments. Shouldn't the import fail in those cases? A way to solve that is by using dynamic imports.

import("react-native-mmkv-storage")
  .then(({ MMKVLoader }) => {
    // do something with the imported modules
  })
  .catch((error: unknown) => {
    // handle whenever the module is not present
  });


const storage = new MMKVLoader().initialize();
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should initialize the store only when the autoStart option is set to always | once, otherwise it makes no sense to initialize something that is not going to be used 😉


export default storage;
Comment on lines +3 to +5
Copy link
Member

Choose a reason for hiding this comment

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

We should try to avoid default exports in favor of named exports 🙂

Suggested change
const storage = new MMKVLoader().initialize();
export default storage;
export const storage = new MMKVLoader().initialize();

1 change: 1 addition & 0 deletions package/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export {
SpotlightTour,
TourStep,
useSpotlightTour,
AutoStartOptions,
} from "./lib/SpotlightTour.context";
export {
SpotlightTourProvider,
Expand Down
5 changes: 5 additions & 0 deletions package/src/lib/SpotlightTour.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export enum Position {
*/
export type Motion = "bounce" | "slide" | "fade";

/**
* Possible tour autostart options
*/
export type AutoStartOptions = "never" | "always" | "once";

export interface RenderProps {
/**
* The index of the current step the tour is on.
Expand Down
42 changes: 39 additions & 3 deletions package/src/lib/SpotlightTour.provider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from "react";
import hash from "object-hash";
import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState, useEffect } from "react";
import { ColorValue, LayoutRectangle } from "react-native";
import { useMMKVStorage } from "react-native-mmkv-storage";

import { ChildFn, isChildFunction } from "../helpers/common";
import storage from "../helpers/storage";

import {
AutoStartOptions,
BackdropPressBehavior,
Motion,
OSConfig,
Expand All @@ -18,6 +22,12 @@ import {
import { TourOverlay, TourOverlayRef } from "./components/tour-overlay/TourOverlay.component";

export interface SpotlightTourProviderProps {
/**
* Sets the default behaviour when the tour starts.
*
* @default never
*/
autoStart?: AutoStartOptions; // never - always - once
/**
* The children to render in the provider. It accepts either a React
* component, or a function that returns a React component. When the child is
Expand Down Expand Up @@ -54,6 +64,12 @@ export interface SpotlightTourProviderProps {
*/
onBackdropPress?: BackdropPressBehavior;
/**
* Handler which gets executed when {@link SpotlightTour.start|start} is
* invoked. It receives the {@link SpotlightTour.current|current} step index
* so you can access the current step where the tour starts.
*/
onStart?: () => void;
/*
* Handler which gets executed when {@link SpotlightTour.stop|stop} is
* invoked. It receives the {@link StopParams} so
* you can access the `current` step index where the tour stopped
Expand Down Expand Up @@ -92,6 +108,7 @@ export interface SpotlightTourProviderProps {
*/
export const SpotlightTourProvider = forwardRef<SpotlightTour, SpotlightTourProviderProps>((props, ref) => {
const {
autoStart = "never",
children,
motion = "bounce",
nativeDriver = true,
Expand All @@ -101,10 +118,12 @@ export const SpotlightTourProvider = forwardRef<SpotlightTour, SpotlightTourProv
overlayOpacity = 0.45,
spotPadding = 16,
steps,
onStart,
} = props;

const [current, setCurrent] = useState<number>();
const [spot, setSpot] = useState(ZERO_SPOT);
const [tourId, setTourId] = useMMKVStorage("tourId", storage, "");
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we don't need to use the hook here. We don't need to use the tourId as a state. We only need to get/set it upon the tour start call


const overlay = useRef<TourOverlayRef>({
hideTooltip: () => Promise.resolve({ finished: false }),
Expand All @@ -118,7 +137,7 @@ export const SpotlightTourProvider = forwardRef<SpotlightTour, SpotlightTourProv
overlay.current.hideTooltip(),
Promise.resolve().then(step.before),
])
.then(() => setCurrent(index));
.then(() => setCurrent(index));
}
}, [steps]);

Expand All @@ -128,7 +147,24 @@ export const SpotlightTourProvider = forwardRef<SpotlightTour, SpotlightTourProv

const start = useCallback((): void => {
renderStep(0);
}, [renderStep]);
onStart?.();
}, [renderStep, onStart]);

const startOnce = useCallback(() => {
if (!tourId) {
setTourId(hash(steps));
Copy link
Member

Choose a reason for hiding this comment

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

What happens if the user has multiple tour providers in their app. I think we will need them to provide an ID of the tour to use this feature, and the value we check against is the hash of the steps. What do you think? 🤔

renderStep(0);
onStart?.();
Comment on lines +156 to +157
Copy link
Member

Choose a reason for hiding this comment

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

I think these two can be replaced by calling the start() function

}
}, [renderStep, onStart, steps]);

useEffect(() => {
if (autoStart === "always") {
start();
} else if (autoStart === "once") {
startOnce();
}
}, [renderStep, autoStart]);

const stop = useCallback((): void => {
setCurrent(prev => {
Expand Down
15 changes: 12 additions & 3 deletions package/test/helpers/TestTour.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import React from "react";
import { Button, Text, TouchableOpacity, View } from "react-native";

import { Align, AttachStep, Position, SpotlightTourProvider, TourStep, useSpotlightTour } from "../../src";
import {
Align,
AttachStep,
Position,
SpotlightTourProvider,
TourStep,
useSpotlightTour,
AutoStartOptions,
} from "../../src";

interface TestScreenProps {
autoStart?: AutoStartOptions;
steps?: TourStep[];
}

Expand Down Expand Up @@ -54,9 +63,9 @@ const defaultSteps = [
{ ...BASE_STEP, position: Position.TOP },
];

export const TestScreen: React.FC<TestScreenProps> = ({ steps }) => {
export const TestScreen: React.FC<TestScreenProps> = ({ steps, autoStart }) => {
return (
<SpotlightTourProvider steps={steps ?? defaultSteps}>
<SpotlightTourProvider steps={steps ?? defaultSteps} autoStart={autoStart ?? "never"}>
<TestComponent />
</SpotlightTourProvider>
);
Expand Down
34 changes: 34 additions & 0 deletions package/test/integration/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,4 +285,38 @@ describe("[Integration] index.test.tsx", () => {
});
});
});

describe("autoStart property", () => {
describe("when the autoStart property is set to never", () => {
it("the overlay is not shown", async () => {
const { getByText, queryByTestId } = render(<TestScreen autoStart="never" />);
await waitFor(() => expect(getByText("Start")).toBePresent());
expect(queryByTestId("Overlay View")).toBeNull();
});
});

describe("when the autoStart property is set to always", () => {
it("shows the overlay view", async () => {
const { getByTestId } = render(<TestScreen autoStart="always" />);
await waitFor(() => expect(getByTestId("Overlay View")).toBePresent());
});
});

describe("when the autoStart property is set to once", () => {
describe("when the device is not registered", () => {
it("shows the overlay view", async() => {
const { getByTestId } = render(<TestScreen autoStart="once" />);
await waitFor(() => expect(getByTestId("Overlay View")).toBePresent());
});
});
describe("when the device is already registered", () => {
it("the overlay is not shown", async () => {
const { queryByTestId } = render(<TestScreen autoStart="once" />);
await waitFor(() => {
expect(queryByTestId("Overlay View")).toBeNull();
});
});
});
});
});
});
15 changes: 14 additions & 1 deletion package/test/setup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable max-classes-per-file */
import { useState } from "react";
import { Animated, LayoutRectangle } from "react-native";
import { MMKVInstance } from "react-native-mmkv-storage";

import {
isAnimatedTimingInterpolation,
Expand Down Expand Up @@ -136,7 +138,18 @@ jest
timing: timingMock,
},
};
});
})
/* eslint-disable @typescript-eslint/no-unused-vars */
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this? 🤔

.mock("react-native-mmkv-storage", () => ({
MMKVLoader: jest.fn().mockImplementation(() => ({
initialize: () => jest.fn(),
})),
useMMKVStorage: (_key: string, _storage: MMKVInstance, defaultValue: string) => {
const [value, setValue] = useState(defaultValue);
const setMockValue = (newValue: string): void => setValue(newValue);
return [value, setMockValue];
},
}));

afterEach(() => {
jest.resetAllMocks();
Expand Down
30 changes: 30 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3275,6 +3275,7 @@ __metadata:
"@testing-library/react-native": ^11.5.4
"@types/jest": ^29.4.1
"@types/node": ^18.15.3
"@types/object-hash": ^3.0.2
"@types/react-test-renderer": ^18.0.0
"@types/sinon": ^10.0.13
"@types/styled-components": ^5.1.26
Expand All @@ -3284,9 +3285,11 @@ __metadata:
fast-equals: ^5.0.1
jest: ^29.5.0
metro-react-native-babel-preset: ^0.76.1
object-hash: ^3.0.0
react: ^18.2.0
react-is: ^18.2.0
react-native: 0.71.6
react-native-mmkv-storage: ^0.9.1
react-native-responsive-dimensions: ^3.1.1
react-native-svg: ^13.8.0
react-test-renderer: ^18.2.0
Expand All @@ -3303,6 +3306,7 @@ __metadata:
peerDependencies:
react: ">=16.8.0"
react-native: ">=0.50.0"
react-native-mmkv-storage: ">=0.9.1"
react-native-svg: ">=12.1.0"
peerDependenciesMeta:
react:
Expand Down Expand Up @@ -3513,6 +3517,13 @@ __metadata:
languageName: node
linkType: hard

"@types/object-hash@npm:^3.0.2":
version: 3.0.2
resolution: "@types/object-hash@npm:3.0.2"
checksum: 0332e59074e7df2e74c093a7419c05c1e1c5ae1e12d3779f3240b3031835ff045b4ac591362be0b411ace24d3b5e760386b434761c33af25904f7a3645cb3785
languageName: node
linkType: hard

"@types/parse-json@npm:^4.0.0":
version: 4.0.0
resolution: "@types/parse-json@npm:4.0.0"
Expand Down Expand Up @@ -6504,6 +6515,7 @@ __metadata:
react: 18.2.0
react-is: ^18.2.0
react-native: 0.71.6
react-native-mmkv-storage: ^0.9.1
react-native-svg: ^13.8.0
styled-components: ^5.3.9
typescript: ^4.9.5
Expand Down Expand Up @@ -10857,6 +10869,13 @@ __metadata:
languageName: node
linkType: hard

"object-hash@npm:^3.0.0":
version: 3.0.0
resolution: "object-hash@npm:3.0.0"
checksum: 80b4904bb3857c52cc1bfd0b52c0352532ca12ed3b8a6ff06a90cd209dfda1b95cee059a7625eb9da29537027f68ac4619363491eedb2f5d3dddbba97494fd6c
languageName: node
linkType: hard

"object-inspect@npm:^1.12.3, object-inspect@npm:^1.9.0":
version: 1.12.3
resolution: "object-inspect@npm:1.12.3"
Expand Down Expand Up @@ -11785,6 +11804,17 @@ __metadata:
languageName: node
linkType: hard

"react-native-mmkv-storage@npm:^0.9.1":
version: 0.9.1
resolution: "react-native-mmkv-storage@npm:0.9.1"
peerDependencies:
react-native: "*"
bin:
mmkv-link: autolink/postlink/run.js
checksum: 564f1cd971d20c9db03cff2d1cf8d55a2c69bd5a907eebbee7bcdc0592917bea5c787b424b1910931c649b3f17c9a361385565af585ec72fb7d1b63b9f2570af
languageName: node
linkType: hard

"react-native-responsive-dimensions@npm:^3.1.1":
version: 3.1.1
resolution: "react-native-responsive-dimensions@npm:3.1.1"
Expand Down