Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Add autoStart feature #138

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
14 changes: 14 additions & 0 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ PODS:
- hermes-engine/Pre-built (= 0.72.4)
- hermes-engine/Pre-built (0.72.4)
- libevent (2.1.12)
- MMKV (1.2.13):
- MMKVCore (~> 1.2.13)
- MMKVCore (1.2.16)
- OpenSSL-Universal (1.1.1100)
- RCT-Folly (2021.07.22.00):
- boost
Expand Down Expand Up @@ -375,6 +378,9 @@ PODS:
- React-jsinspector (0.72.4)
- React-logger (0.72.4):
- glog
- react-native-mmkv-storage (0.9.1):
- MMKV (= 1.2.13)
- React-Core
- React-NativeModulesApple (0.72.4):
- hermes-engine
- React-callinvoker
Expand Down Expand Up @@ -538,6 +544,7 @@ DEPENDENCIES:
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- react-native-mmkv-storage (from `../node_modules/react-native-mmkv-storage`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
Expand Down Expand Up @@ -571,6 +578,8 @@ SPEC REPOS:
- FlipperKit
- fmt
- libevent
- MMKV
- MMKVCore
- OpenSSL-Universal
- SocketRocket
- YogaKit
Expand Down Expand Up @@ -619,6 +628,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/jsinspector"
React-logger:
:path: "../node_modules/react-native/ReactCommon/logger"
react-native-mmkv-storage:
:path: "../node_modules/react-native-mmkv-storage"
React-NativeModulesApple:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
React-perflogger:
Expand Down Expand Up @@ -676,6 +687,8 @@ SPEC CHECKSUMS:
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: 81191603c4eaa01f5e4ae5737a9efcf64756c7b2
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
MMKV: aac95d817a100479445633f2b3ed8961b4ac5043
MMKVCore: 9cfef4c48c6c46f66226fc2e4634d78490206a48
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: c0569ecc035894e4a68baecb30fe6a7ea6e399f9
Expand All @@ -692,6 +705,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: c7f826e40fa9cab5d37cab6130b1af237332b594
React-jsinspector: aaed4cf551c4a1c98092436518c2d267b13a673f
React-logger: da1ebe05ae06eb6db4b162202faeafac4b435e77
react-native-mmkv-storage: cfb6854594cfdc5f7383a9e464bb025417d1721c
React-NativeModulesApple: edb5ace14f73f4969df6e7b1f3e41bef0012740f
React-perflogger: 496a1a3dc6737f964107cb3ddae7f9e265ddda58
React-RCTActionSheet: 02904b932b50e680f4e26e7a686b33ebf7ef3c00
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"react-dom": "^18.2.0",
"react-is": "^18.2.0",
"react-native": "0.72.4",
"react-native-mmkv-storage": "^0.9.1",
"react-native-spotlight-tour": "workspace:^",
"react-native-svg": "^13.14.0",
"react-native-web": "^0.19.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 @@ -117,6 +117,7 @@ export function App(): ReactElement {
}}
onBackdropPress="continue"
onStop={onStopTour}
autoStart="always"
motion="bounce"
shape="circle"
>
Expand Down
4 changes: 4 additions & 0 deletions package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
"packageManager": "[email protected]",
"dependencies": {
"@floating-ui/react-native": "^0.10.1",
"object-hash": "^3.0.0",
"react-fast-compare": "^3.2.2",
"react-native-mmkv-storage": "^0.9.1",
"react-native-responsive-dimensions": "^3.1.1",
"styled-components": "^6.0.8"
},
Expand All @@ -48,6 +50,7 @@
"@testing-library/react-native": "^12.3.0",
"@types/jest": "^29.5.5",
"@types/node": "^20.6.3",
"@types/object-hash": "^3.0.6",
"@types/react-test-renderer": "^18.0.2",
"@types/sinon": "^10.0.16",
"babel-jest": "^29.7.0",
Expand All @@ -73,6 +76,7 @@
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
"react-native": ">=0.50.0",
"react-native-mmkv-storage": ">=0.9.1",
"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";

const storage = new MMKVLoader().initialize();

export default storage;
1 change: 1 addition & 0 deletions package/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {
StopParams,
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 @@ -10,6 +10,11 @@ import { LayoutRectangle } from "react-native";
*/
export type Motion = "bounce" | "slide" | "fade";

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

/**
* Possible shape for the tour spotlight:
* - `circle`
Expand Down
47 changes: 38 additions & 9 deletions package/src/lib/SpotlightTour.provider.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { flip, offset, shift } from "@floating-ui/react-native";
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 @@ -27,6 +24,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 @@ -71,6 +74,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 @@ -116,6 +125,7 @@ export interface SpotlightTourProviderProps {
*/
export const SpotlightTourProvider = forwardRef<SpotlightTour, SpotlightTourProviderProps>((props, ref) => {
const {
autoStart = "never",
children,
floatingProps = {
middleware: [flip(), offset(4), shift()],
Expand All @@ -130,10 +140,12 @@ export const SpotlightTourProvider = forwardRef<SpotlightTour, SpotlightTourProv
shape = "circle",
spotPadding = 16,
steps,
onStart,
} = props;

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

const overlay = useRef<TourOverlayRef>({
hideTooltip: () => Promise.resolve({ finished: false }),
Expand All @@ -156,7 +168,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));
renderStep(0);
onStart?.();
}
}, [renderStep, onStart, steps]);

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

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

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

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

Expand Down Expand Up @@ -55,9 +56,9 @@ const defaultSteps = [
{ ...BASE_STEP },
];

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 @@ -222,4 +222,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 */
.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 @@ -3811,6 +3811,13 @@ __metadata:
languageName: node
linkType: hard

"@types/object-hash@npm:^3.0.6":
version: 3.0.6
resolution: "@types/object-hash@npm:3.0.6"
checksum: 2c7979d4e540af817b99c09fb4c2fed1c0b0e14342df474d8dcde4165a82c440b038341fd66fe998d9f86acdd5cccc65ba8092315e39e7c2114f945fa70aaa56
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 @@ -6923,6 +6930,7 @@ __metadata:
react-dom: ^18.2.0
react-is: ^18.2.0
react-native: 0.72.4
react-native-mmkv-storage: ^0.9.1
react-native-spotlight-tour: "workspace:^"
react-native-svg: ^13.14.0
react-native-web: ^0.19.9
Expand Down Expand Up @@ -11646,6 +11654,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 @@ -12581,6 +12596,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 Expand Up @@ -12623,16 +12649,19 @@ __metadata:
"@testing-library/react-native": ^12.3.0
"@types/jest": ^29.5.5
"@types/node": ^20.6.3
"@types/object-hash": ^3.0.6
"@types/react-test-renderer": ^18.0.2
"@types/sinon": ^10.0.16
babel-jest: ^29.7.0
expect-type: ^0.16.0
jest: ^29.7.0
metro-react-native-babel-preset: ^0.77.0
object-hash: ^3.0.0
react: ^18.2.0
react-fast-compare: ^3.2.2
react-is: ^18.2.0
react-native: 0.72.4
react-native-mmkv-storage: ^0.9.1
react-native-responsive-dimensions: ^3.1.1
react-native-svg: ^13.14.0
react-test-renderer: ^18.2.0
Expand All @@ -12650,6 +12679,7 @@ __metadata:
react: ">=16.8.0"
react-dom: ">=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
Loading