├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .eslintrc.js ├── .flowconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── release.yml ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── example ├── .gitignore ├── App.jsx ├── app.json ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── eas.json ├── package.json └── yarn.lock ├── jest.config.js ├── package.json ├── src ├── components │ ├── CopilotModal.tsx │ ├── CopilotStep.tsx │ ├── SvgMask.tsx │ ├── ViewMask.tsx │ ├── default-ui │ │ ├── Button.tsx │ │ ├── StepNumber.tsx │ │ └── Tooltip.tsx │ ├── style.ts │ └── tests │ │ └── CopilotStep.test.tsx ├── contexts │ └── CopilotProvider.tsx ├── hocs │ ├── copilot.tsx │ ├── tests │ │ └── walkthroughable.test.tsx │ └── walkthroughable.tsx ├── hooks │ ├── useStateWithAwait.ts │ └── useStepsMap.ts ├── index.ts └── types.ts ├── tsconfig.json ├── tsconfig.spec.json ├── tsup.config.ts └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps define and maintain consistent 2 | # coding styles between different editors and IDEs. 3 | # http://editorconfig.org/ 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | indent_size = 2 10 | end_of_line = lf 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {require("eslint").Linter.Config} */ 2 | module.exports = { 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | }, 7 | extends: [ 8 | "plugin:react/recommended", 9 | "plugin:react-hooks/recommended", 10 | "standard-with-typescript", 11 | "prettier", 12 | ], 13 | overrides: [], 14 | parserOptions: { 15 | ecmaVersion: "latest", 16 | sourceType: "module", 17 | project: ["./tsconfig.json"], 18 | }, 19 | plugins: ["react"], 20 | rules: { 21 | "@typescript-eslint/explicit-function-return-type": "off", 22 | "@typescript-eslint/strict-boolean-expressions": "off", 23 | "@typescript-eslint/no-unsafe-argument": "off", 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [include] 2 | ./src 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mohebifar 2 | patreon: # Replace with a single Patreon username 3 | open_collective: # Replace with a single Open Collective username 4 | ko_fi: # Replace with a single Ko-fi username 5 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 6 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 7 | liberapay: # Replace with a single Liberapay username 8 | issuehunt: # Replace with a single IssueHunt username 9 | otechie: # Replace with a single Otechie username 10 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: 5 | --- 6 | 7 | **Current Behavior** 8 | A clear and concise description of the behavior. 9 | 10 | **Input Code** 11 | 12 | - REPL or Repo link if applicable: 13 | 14 | ```js 15 | var your => (code) => here; 16 | ``` 17 | 18 | **Expected behavior/code** 19 | A clear and concise description of what you expected to happen (or code). 20 | 21 | **Environment** 22 | 23 | - Device: [e.g. iPhone 8 Simulator] 24 | - OS: [e.g. iOS12] 25 | - `react-native-copilot`: [e.g. v2.4.1] 26 | - `react-native`: [e.g. v0.57] 27 | - `react-native-svg`: [e.g. v7.1.0] 28 | 29 | **Possible Solution** 30 | 31 | 32 | 33 | **Additional context/Screenshots** 34 | Add any other context about the problem here. If applicable, add screenshots to help explain. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I have an issue when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. Add any considered drawbacks. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Teachability, Documentation, Adoption, Migration Strategy** 17 | If you can, explain how users will be able to use this and possibly write out a version the docs. 18 | Maybe a screenshot or design? 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup Node.js 18.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 18.x 22 | 23 | - name: Install Dependencies 24 | run: yarn 25 | 26 | - name: Lint 27 | run: yarn lint 28 | 29 | - name: Test 30 | run: yarn test 31 | 32 | - name: Build 33 | run: yarn build 34 | 35 | - name: Create Release Pull Request or Publish to npm 36 | id: changesets 37 | uses: changesets/action@v1 38 | with: 39 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 40 | publish: yarn release 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.* 2 | .DS_Store 3 | node_modules 4 | dist -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | script: 5 | - yarn test 6 | - yarn lint 7 | deploy: 8 | provider: npm 9 | email: "$NPM_AUTHOR" 10 | api_key: "$NPM_TOKEN" 11 | on: 12 | branch: master 13 | env: 14 | global: 15 | - secure: lvFQZnQE/BYH/8OiVDM6QoE44Q67xbJEK6MDXwK0QIWKigjvwfSQRUx3cyMDiX6xUo92Qvmc0PHLzIE+Y6Ozp3fNkuxR1rAam0oiJmr3jgui4dhzFid5x9S+gZrT7KS+8h3PPNrKMSJ1ju5E7CbwdaPu+dEN+fUvfbfUa0VNlgO0Vwk9iWs229aypxo3vwgMMMgi8sS39p6Fr6C2CNMCFCOpVSshxAVnSfNIip+sQpuEdFszgjBrvGMjeslEG2PsagSzhx+liBQFbCrOKuhf0CFtH4utLDfIlr0XrP+boIL2WHwn8EwSbzPEFIbnYaPk5yyaFiI2F0RHTFL01xrsrLwiJD02za9Topb6REmHHT5wrleYExgIup3gWFJofQ7lGA2YwPuCk+68F0YHWDeuC4Is/N0wMgtex9LMQCd66H3CZJk77Pp72csrzI9fURj5QZOOacL+C175E1Il6fJ9OsCu6rhQM9vw/tFYp3YsA7mJaw/RMfEsJrjaA4hZgF8yQvAs4eO8CN/ROGHSbDS5q9K5RjvxkBt3pkJWR555pcpRUmfXpLYkZdNf8XQJF5WEZyAUwLebIZ8uWR0BdYg83EHKZyUyNVyez2wxZQHXYi1eIkiAs9nzQhyzTce60UiZqg4BnpbPtZZkep58fKCSxwM714LmoPWSCckfUlVYS/A= 16 | - secure: aJkIQZhB2ZlI43R3jLM3C5nIMAJ7CytkuOlKJ0L3U5IsCoPxr9+Dxl0VCMivkrtGTjD/eoYJtPjkHWRQ56H0pF/zKWlv5LtUtZFZ4q3oGE33FlIQ7gA20hahn+4AbB3yyk0zlPUF1jrL+RmRGwc/ckeSdE28R/xKfAl7qRrCk41U6amvVjBYzXk37npGLJI31EUz7/IbQ5MB4S2hfrWnkjwr0QfNWm1D9c2YOTWXeYIlO92z3LZWRjv2qSkSudsanEStINL2JNVdwy38pFVPKH0MroISUIZqCQmrLhXidmWOGntlDgAeQMjxjl9T11WFY/Qz2yqYoVgbl+EIoloNRLR3/LNRsOQBEcIRNV/hjwHb8NelsmPhuH6nJ0qN/ErADXnmDEtmVCjQoOsc5A5tuhd7HY0t28qdJXcf3gNoWc7HUdVvPOdTaQAZ755hg3aFn2nSgMOU8Ph0aWjiJWxdE9IaS4fG+UiSThHaKWoFKI+OVJRUMD1csRgIXo5bhAm6WcTSYyR+j36LZ0n9RhYcV1xVERYa9UL/RrbQT0vp9sWHKA5Ahsrzx/MQq13CuWkEh8duc26Td02soVPcnjB07dmehAfttEs9mzdHUEP3VkDZa6mrHOkXZVg/bbD2QuwK0PFymd12frXZVvi6V2C3PCGP57xnqPsYRfCAduKGuUE= 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | <<<<<<< Updated upstream 2 | 3 | ## 3.3.3 4 | 5 | ### Patch Changes 6 | 7 | - 7961855: Fix arrow style on Android 8 | 9 | # Change Log 10 | 11 | ## 3.3.2 12 | 13 | ### Patch Changes 14 | 15 | - d14d956: Expose totalStepsNumber via useCopilot 16 | 17 | ## 3.3.1 18 | 19 | ### Patch Changes 20 | 21 | - 05b3a47: Fix a type error in walkthroughable 22 | 23 | ## 3.3.0 24 | 25 | ### Minor Changes 26 | 27 | - 3e5d3cc: Fix the issue with missing module file specified in package json 28 | 29 | ## 3.2.1 30 | 31 | ### Patch Changes 32 | 33 | - 3a6dd5a: Fix malformed field crash and add more config (arrowSize and margin) 34 | 35 | ## 3.2.0 36 | 37 | ### Minor Changes 38 | 39 | - 0d8362a: Remove Tooltip and StepNumber passed props in favor of useCopilot context 40 | Un-register the step after name change and re-register with the new name 41 | Add tests for CopilotStep 42 | 43 | ## 3.1.0 44 | 45 | ### Minor Changes 46 | 47 | - 312fba4: Expose more functions through the public API 48 | 49 | Expose `stop`, `goToNext`, `goToNth`, and `goToPrev` through the `useCopilot` hook 50 | Export `DefaultUI` from the module's entry to access the default Tooltip and StepNumber components 51 | 52 | ## 3.0.1 53 | 54 | ### Patch Changes 55 | 56 | - 3a0f6e0: Migrate to TS and add CopilotProvider 57 | 58 | ## 3.0.0 59 | 60 | ### Major Changes 61 | 62 | - # f2b45c7: Migrate to TS and deprecate HOC 63 | 64 | # Changelog 65 | 66 | > > > > > > > Stashed changes 67 | 68 | All notable changes to this project will be documented in this file. 69 | 70 | ## [3.3.0] - 2024-03-06 71 | 72 | ### ⚙️ Miscellaneous Tasks 73 | 74 | - Upgrade dependencies and fix the issue with missing module file specified in package json (#311) 75 | 76 | ## [3.2.1] - 2023-04-13 77 | 78 | ### 🐛 Bug Fixes 79 | 80 | - _(android)_ Fix malformed field crash, add more config (#279) 81 | 82 | ## [3.0.0] - 2023-03-22 83 | 84 | ### 🚀 Features 85 | 86 | - _(CopilotStep)_ Add active flag to steps 87 | - _(tooltip)_ Add getNth function (#267) 88 | 89 | ### 🐛 Bug Fixes 90 | 91 | - Fixing the arrow position 92 | - _(CopilotStep)_ Fix measure error where **TEST** is undefined 93 | - _(SvgMask)_ Defer rendering Svg until layout is measured 94 | - _(example)_ Fix the watcher for updating the example dep 95 | - _(copilot)_ Start(fromStep) issue fixed 96 | - _(CopilotModal)_ Consider status bar height 97 | - _(CopilotModal)_ Do status bar offset only on android 98 | - _(contributing)_ Fix broken links 99 | - _(copilot)_ Hoist static props 100 | - _(types)_ Adjust type from handleNthStep -> handleNth (#268) 101 | 102 | ### 🚜 Refactor 103 | 104 | - The Button to the compoenents directory 105 | - _(CopilotModal)_ Use Modal instead of ternary expression to control visibiity 106 | 107 | ### 📚 Documentation 108 | 109 | - _(Readme)_ Improve docs 110 | - Charles suggestions 111 | - _(Readme)_ Add badges 112 | - Update semaphore project name 113 | - Remove old name inspiration 114 | - _(README)_ Renaming the npm package 115 | - _(README)_ Copilotable to walkthroughable 116 | - _(README)_ Add tutorial triggering description 117 | - _(README)_ Updates for v2.0.0 118 | - _(README)_ Add react-native-svg to the installation section 119 | - Fix shields to point to correct pkg & center 120 | - _(events)_ Described event emitters in readme 121 | - _(events)_ Remove event handlers on unmount in example 122 | - _(README)_ Add OKG 123 | - _(README)_ Update OKGrow link 124 | 125 | ### 🎨 Styling 126 | 127 | - Fix lint errors 128 | - Lint 129 | 130 | ### 🧪 Testing 131 | 132 | - Add unit tests 133 | - _(walkthroughable)_ Added tests for the walkthroughable 134 | - _(copilot)_ Added tests for the copilot HOC 135 | - _(copilot)_ Add test for tooltip component 136 | - _(copilot)_ Update the tests based on the Modal change 137 | - _(Copilot)_ Test for the step active flags 138 | 139 | ### ⚙️ Miscellaneous Tasks 140 | 141 | - Add .editorconfig 142 | - Change the package author 143 | - Eslint 144 | - Rename the package 145 | - Add package lock 146 | - Bump version (1.0.1) 147 | - _(example)_ Upgrade expo for the example and better src linking 148 | - Cleaning up the directory structure 149 | - _(Example)_ Update name of the package in the example 150 | - _(yarn)_ Upgrade packages and remove package-lock.json 151 | - _(package)_ Bump version to 2.1.0 152 | - _(README)_ Update badges 153 | - _(package)_ Bump version 2.2.0 154 | - _(package)_ Bump version to 2.2.3 155 | - _(package)_ Bump the version to 2.2.5 156 | - _(package)_ Bump version 2.2.6 157 | - _(package)_ Bump version to 2.3.0 158 | - Add hoist-non-react-statics dependency 159 | - Add `hoist-non-react-statics` dependency 160 | - Bump version to 2.4.0 161 | - Bump version to 2.4.1 162 | - Lint 163 | - _(package)_ Bump version 164 | - _(package)_ Update version 165 | - Add travis 166 | - Upgrade packages 167 | - More pkg upgrades 168 | - Fix a lint issue 169 | - Upgrade expo to v34 170 | - _(travis)_ Run deploy on master 171 | - _(package)_ Bump version to 2.4.8 172 | 173 | ### Example 174 | 175 | - _(App)_ Trigger tutorial on componentDidMount 176 | - _(App)_ Added an example for the event emitter 177 | - Added the active flag to the example 178 | 179 | 180 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Issues and Pull Requests are always welcome. 2 | 3 | Please read OK Grow's global [contribution guidelines](https://github.com/okgrow/guides/blob/master/docs/OpenSource-Contributing.md). 4 | 5 | If you are interested in becoming a maintainer, get in touch with us by sending an email or opening an issue. You should already have code merged into the project. Active contributors are encouraged to get in touch. 6 | 7 | Please note that all interactions in @okgrow's repos should follow our [Code of Conduct](https://github.com/okgrow/guides/blob/master/docs/OpenSource-CodeOfConduct.md). 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 OK GROW! 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

React Native Copilot

2 | 3 |
4 |

5 | 6 | Build Status 7 | 8 | 9 | NPM Version 10 | 11 | 12 | NPM Downloads 13 | 14 |

15 |
16 | 17 |

18 | Step-by-step walkthrough for your react native app! 19 |

20 | 21 |

22 | React Native Copilot 23 |

24 | 25 |

26 | 27 | Demo 28 | 29 |

30 | 31 | ## Installation 32 | 33 | ``` 34 | yarn add react-native-copilot 35 | 36 | # or with npm: 37 | 38 | npm install --save react-native-copilot 39 | ``` 40 | 41 | **Optional**: If you want to have the smooth SVG animation, you should install and link [`react-native-svg`](https://github.com/software-mansion/react-native-svg). 42 | 43 | ## Usage 44 | 45 | Wrap the portion of your app that you want to use copilot with inside ``: 46 | 47 | ```js 48 | import { CopilotProvider } from "react-native-copilot"; 49 | 50 | const AppWithCopilot = () => { 51 | return ( 52 | 53 | 54 | 55 | ); 56 | }; 57 | ``` 58 | 59 | **NOTE**: The old way of using copilot with the `copilot()` HOC maker is deprecated in v3. It will continue to work but it's not recommended and may be removed in the future. 60 | 61 | Before defining walkthrough steps for your react elements, you must make them `walkthroughable`. The easiest way to do that for built-in react native components, is using the `walkthroughable` HOC. Then you must wrap the element with `CopilotStep`. 62 | 63 | ```jsx 64 | import { 65 | CopilotProvider, 66 | CopilotStep, 67 | walkthroughable, 68 | } from "react-native-copilot"; 69 | 70 | const CopilotText = walkthroughable(Text); 71 | 72 | const HomeScreen = () => { 73 | return ( 74 | 75 | 76 | Hello world! 77 | 78 | 79 | ); 80 | }; 81 | ``` 82 | 83 | Every `CopilotStep` must have these props: 84 | 85 | 1. **name**: A unique name for the walkthrough step. 86 | 2. **order**: A positive number indicating the order of the step in the entire walkthrough. 87 | 3. **text**: The text shown as the description for the step. 88 | 89 | Additionally, a step may set the **active** prop, a boolean that controls whether the step is used or skipped. 90 | 91 | In order to start the tutorial, you can call the `start` function from the `useCopilot` hook: 92 | 93 | ```js 94 | const HomeScreen = () => { 95 | return ( 96 | 97 | 37 | 38 | ) : null} 39 | {!isFirstStep ? ( 40 | 41 | 42 | 43 | ) : null} 44 | {!isLastStep ? ( 45 | 46 | 47 | 48 | ) : ( 49 | 50 | 51 | 52 | )} 53 | 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/style.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export const STEP_NUMBER_RADIUS: number = 14; 4 | export const STEP_NUMBER_DIAMETER: number = STEP_NUMBER_RADIUS * 2; 5 | export const ZINDEX: number = 100; 6 | export const MARGIN: number = 13; 7 | export const OFFSET_WIDTH: number = 4; 8 | export const ARROW_SIZE: number = 6; 9 | 10 | export const styles = StyleSheet.create({ 11 | container: { 12 | position: "absolute", 13 | left: 0, 14 | top: 0, 15 | right: 0, 16 | bottom: 0, 17 | zIndex: ZINDEX, 18 | }, 19 | arrow: { 20 | position: "absolute", 21 | borderWidth: ARROW_SIZE, 22 | }, 23 | tooltip: { 24 | position: "absolute", 25 | paddingTop: 15, 26 | paddingHorizontal: 15, 27 | backgroundColor: "#fff", 28 | borderRadius: 3, 29 | overflow: "hidden", 30 | }, 31 | tooltipText: {}, 32 | tooltipContainer: { 33 | flex: 1, 34 | }, 35 | stepNumberContainer: { 36 | position: "absolute", 37 | width: STEP_NUMBER_DIAMETER, 38 | height: STEP_NUMBER_DIAMETER, 39 | overflow: "hidden", 40 | zIndex: ZINDEX + 1, 41 | }, 42 | stepNumber: { 43 | flex: 1, 44 | alignItems: "center", 45 | justifyContent: "center", 46 | borderWidth: 2, 47 | borderRadius: STEP_NUMBER_RADIUS, 48 | borderColor: "#FFFFFF", 49 | backgroundColor: "#27ae60", 50 | }, 51 | stepNumberText: { 52 | fontSize: 10, 53 | backgroundColor: "transparent", 54 | color: "#FFFFFF", 55 | }, 56 | button: { 57 | padding: 10, 58 | }, 59 | buttonText: { 60 | color: "#27ae60", 61 | }, 62 | bottomBar: { 63 | marginTop: 10, 64 | flexDirection: "row", 65 | justifyContent: "flex-end", 66 | }, 67 | overlayRectangle: { 68 | position: "absolute", 69 | backgroundColor: "rgba(0,0,0,0.2)", 70 | left: 0, 71 | top: 0, 72 | bottom: 0, 73 | right: 0, 74 | }, 75 | overlayContainer: { 76 | position: "absolute", 77 | left: 0, 78 | top: 0, 79 | bottom: 0, 80 | right: 0, 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /src/components/tests/CopilotStep.test.tsx: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-native/extend-expect"; 2 | import { render, screen } from "@testing-library/react-native"; 3 | import React from "react"; 4 | import { 5 | CopilotProvider, 6 | type useCopilot, 7 | } from "../../contexts/CopilotProvider"; 8 | import { CopilotStep } from "../CopilotStep"; 9 | 10 | jest.mock("../../contexts/CopilotProvider", () => ({ 11 | ...jest.requireActual("../../contexts/CopilotProvider"), 12 | useCopilot: jest 13 | .fn() 14 | .mockImplementation( 15 | () => jest.requireActual("../../contexts/CopilotProvider").useCopilot 16 | ), 17 | })); 18 | 19 | const actualUseCopilot = jest.requireActual("../../contexts/CopilotProvider") 20 | .useCopilot as typeof useCopilot; 21 | 22 | const mockUseCopilot = jest.requireMock("../../contexts/CopilotProvider") 23 | .useCopilot as jest.Mock>; 24 | 25 | describe("CopilotStep", () => { 26 | beforeEach(() => { 27 | mockUseCopilot.mockClear().mockImplementation(actualUseCopilot); 28 | }); 29 | 30 | test("passes the copilot prop to the child component", async () => { 31 | const WrappedComponent = () => null; 32 | 33 | render( 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | const wrappedComponentElement = screen.UNSAFE_getByType(WrappedComponent); 41 | 42 | expect(wrappedComponentElement.props).toMatchObject({ 43 | copilot: { 44 | onLayout: expect.any(Function), 45 | ref: expect.any(Object), 46 | }, 47 | }); 48 | }); 49 | 50 | test("registers the step", async () => { 51 | const WrappedComponent = () => null; 52 | const registerStepSpy = jest.fn(); 53 | 54 | mockUseCopilot.mockReturnValue({ 55 | registerStep: registerStepSpy, 56 | unregisterStep: jest.fn(), 57 | stop: jest.fn(), 58 | } as any); 59 | 60 | render( 61 | 62 | <> 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | 73 | expect(registerStepSpy).toHaveBeenCalledWith({ 74 | measure: expect.any(Function), 75 | name: "Step 1", 76 | text: "Hello! This is step 1!", 77 | order: 0, 78 | visible: expect.any(Boolean), 79 | wrapperRef: expect.any(Object), 80 | }); 81 | 82 | expect(registerStepSpy).toHaveBeenCalledWith({ 83 | measure: expect.any(Function), 84 | name: "Step 2", 85 | text: "And this is step 2", 86 | order: 1, 87 | visible: expect.any(Boolean), 88 | wrapperRef: expect.any(Object), 89 | }); 90 | }); 91 | 92 | test("re-registers the step after text update", async () => { 93 | const WrappedComponent = () => null; 94 | const registerStepSpy = jest.fn(); 95 | 96 | mockUseCopilot.mockReturnValue({ 97 | registerStep: registerStepSpy, 98 | unregisterStep: jest.fn(), 99 | stop: jest.fn(), 100 | } as any); 101 | 102 | render( 103 | 104 | 105 | 106 | 107 | 108 | ); 109 | 110 | screen.rerender( 111 | 112 | 117 | 118 | 119 | 120 | ); 121 | 122 | expect(registerStepSpy).toHaveBeenCalledWith({ 123 | measure: expect.any(Function), 124 | name: "Step 1", 125 | text: "Hello! This is the same step with updated text!", 126 | order: 0, 127 | visible: expect.any(Boolean), 128 | wrapperRef: expect.any(Object), 129 | }); 130 | }); 131 | 132 | test("unregisters the step after unmount", async () => { 133 | const WrappedComponent = () => null; 134 | const registerStepSpy = jest.fn(); 135 | const unregisterStepSpy = jest.fn(); 136 | 137 | mockUseCopilot.mockReturnValue({ 138 | registerStep: registerStepSpy, 139 | unregisterStep: unregisterStepSpy, 140 | stop: jest.fn(), 141 | } as any); 142 | 143 | render( 144 | 145 | 146 | 147 | 148 | 149 | ); 150 | 151 | // Remove the step from the tree 152 | screen.rerender(); 153 | 154 | expect(unregisterStepSpy).toHaveBeenCalledWith("Step 1"); 155 | }); 156 | 157 | test("unregisters the step after name change and re-registers with the new name", async () => { 158 | const WrappedComponent = () => null; 159 | const registerStepSpy = jest.fn(); 160 | const unregisterStepSpy = jest.fn(); 161 | 162 | mockUseCopilot.mockReturnValue({ 163 | registerStep: registerStepSpy, 164 | unregisterStep: unregisterStepSpy, 165 | stop: jest.fn(), 166 | } as any); 167 | 168 | const stepText = "Hello! This is step 1!"; 169 | 170 | render( 171 | 172 | 173 | 174 | 175 | 176 | ); 177 | 178 | screen.rerender( 179 | 180 | 181 | 182 | 183 | 184 | ); 185 | 186 | expect(unregisterStepSpy).toHaveBeenCalledWith("Step 1"); 187 | 188 | expect(registerStepSpy).toHaveBeenCalledWith({ 189 | measure: expect.any(Function), 190 | name: "Step 1 Updated Name", 191 | text: stepText, 192 | order: 0, 193 | visible: expect.any(Boolean), 194 | wrapperRef: expect.any(Object), 195 | }); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /src/contexts/CopilotProvider.tsx: -------------------------------------------------------------------------------- 1 | import mitt, { type Emitter } from "mitt"; 2 | import React, { 3 | createContext, 4 | useCallback, 5 | useContext, 6 | useMemo, 7 | useRef, 8 | useState, 9 | type PropsWithChildren, 10 | } from "react"; 11 | import { findNodeHandle, type ScrollView } from "react-native"; 12 | import { 13 | CopilotModal, 14 | type CopilotModalHandle, 15 | } from "../components/CopilotModal"; 16 | import { OFFSET_WIDTH } from "../components/style"; 17 | import { useStateWithAwait } from "../hooks/useStateWithAwait"; 18 | import { useStepsMap } from "../hooks/useStepsMap"; 19 | import { type CopilotOptions, type Step } from "../types"; 20 | 21 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions 22 | type Events = { 23 | start: undefined; 24 | stop: undefined; 25 | stepChange: Step | undefined; 26 | }; 27 | 28 | interface CopilotContextType { 29 | registerStep: (step: Step) => void; 30 | unregisterStep: (stepName: string) => void; 31 | currentStep: Step | undefined; 32 | start: ( 33 | fromStep?: string, 34 | suppliedScrollView?: ScrollView | null 35 | ) => Promise; 36 | stop: () => Promise; 37 | goToNext: () => Promise; 38 | goToNth: (n: number) => Promise; 39 | goToPrev: () => Promise; 40 | visible: boolean; 41 | copilotEvents: Emitter; 42 | isFirstStep: boolean; 43 | isLastStep: boolean; 44 | currentStepNumber: number; 45 | totalStepsNumber: number; 46 | } 47 | 48 | /* 49 | This is the maximum wait time for the steps to be registered before starting the tutorial 50 | At 60fps means 2 seconds 51 | */ 52 | const MAX_START_TRIES = 120; 53 | 54 | const CopilotContext = createContext(undefined); 55 | 56 | export const CopilotProvider = ({ 57 | verticalOffset = 0, 58 | children, 59 | ...rest 60 | }: PropsWithChildren) => { 61 | const startTries = useRef(0); 62 | const copilotEvents = useRef(mitt()).current; 63 | const modal = useRef(null); 64 | 65 | const [visible, setVisibility] = useStateWithAwait(false); 66 | const [scrollView, setScrollView] = useState(null); 67 | 68 | const { 69 | currentStep, 70 | currentStepNumber, 71 | totalStepsNumber, 72 | getFirstStep, 73 | getPrevStep, 74 | getNextStep, 75 | getNthStep, 76 | isFirstStep, 77 | isLastStep, 78 | setCurrentStepState, 79 | steps, 80 | registerStep, 81 | unregisterStep, 82 | } = useStepsMap(); 83 | 84 | const moveModalToStep = useCallback( 85 | async (step: Step) => { 86 | const size = await step?.measure(); 87 | 88 | if (!size) { 89 | return; 90 | } 91 | 92 | await modal.current?.animateMove({ 93 | width: size.width + OFFSET_WIDTH, 94 | height: size.height + OFFSET_WIDTH, 95 | x: size.x - OFFSET_WIDTH / 2, 96 | y: size.y - OFFSET_WIDTH / 2 + verticalOffset, 97 | }); 98 | }, 99 | [verticalOffset] 100 | ); 101 | 102 | const setCurrentStep = useCallback( 103 | async (step?: Step, move: boolean = true) => { 104 | setCurrentStepState(step); 105 | copilotEvents.emit("stepChange", step); 106 | 107 | if (scrollView != null) { 108 | const nodeHandle = findNodeHandle(scrollView); 109 | if (nodeHandle) { 110 | step?.wrapperRef.current?.measureLayout( 111 | nodeHandle, 112 | (_x, y, _w, h) => { 113 | const yOffset = y > 0 ? y - h / 2 : 0; 114 | scrollView.scrollTo({ y: yOffset, animated: false }); 115 | } 116 | ); 117 | } 118 | } 119 | 120 | setTimeout( 121 | () => { 122 | if (move && step) { 123 | void moveModalToStep(step); 124 | } 125 | }, 126 | scrollView != null ? 100 : 0 127 | ); 128 | }, 129 | [copilotEvents, moveModalToStep, scrollView, setCurrentStepState] 130 | ); 131 | 132 | const start = useCallback( 133 | async (fromStep?: string, suppliedScrollView: ScrollView | null = null) => { 134 | if (scrollView == null) { 135 | setScrollView(suppliedScrollView); 136 | } 137 | 138 | const currentStep = fromStep ? steps[fromStep] : getFirstStep(); 139 | 140 | if (startTries.current > MAX_START_TRIES) { 141 | startTries.current = 0; 142 | return; 143 | } 144 | 145 | if (currentStep == null) { 146 | startTries.current += 1; 147 | requestAnimationFrame(() => { 148 | void start(fromStep); 149 | }); 150 | } else { 151 | copilotEvents.emit("start"); 152 | await setCurrentStep(currentStep); 153 | await moveModalToStep(currentStep); 154 | await setVisibility(true); 155 | startTries.current = 0; 156 | } 157 | }, 158 | [ 159 | copilotEvents, 160 | getFirstStep, 161 | moveModalToStep, 162 | scrollView, 163 | setCurrentStep, 164 | setVisibility, 165 | steps, 166 | ] 167 | ); 168 | 169 | const stop = useCallback(async () => { 170 | await setVisibility(false); 171 | copilotEvents.emit("stop"); 172 | }, [copilotEvents, setVisibility]); 173 | 174 | const next = useCallback(async () => { 175 | await setCurrentStep(getNextStep()); 176 | }, [getNextStep, setCurrentStep]); 177 | 178 | const nth = useCallback( 179 | async (n: number) => { 180 | await setCurrentStep(getNthStep(n)); 181 | }, 182 | [getNthStep, setCurrentStep] 183 | ); 184 | 185 | const prev = useCallback(async () => { 186 | await setCurrentStep(getPrevStep()); 187 | }, [getPrevStep, setCurrentStep]); 188 | 189 | const value = useMemo( 190 | () => ({ 191 | registerStep, 192 | unregisterStep, 193 | currentStep, 194 | start, 195 | stop, 196 | visible, 197 | copilotEvents, 198 | goToNext: next, 199 | goToNth: nth, 200 | goToPrev: prev, 201 | isFirstStep, 202 | isLastStep, 203 | currentStepNumber, 204 | totalStepsNumber, 205 | }), 206 | [ 207 | registerStep, 208 | unregisterStep, 209 | currentStep, 210 | start, 211 | stop, 212 | visible, 213 | copilotEvents, 214 | next, 215 | nth, 216 | prev, 217 | isFirstStep, 218 | isLastStep, 219 | currentStepNumber, 220 | totalStepsNumber, 221 | ] 222 | ); 223 | 224 | return ( 225 | 226 | <> 227 | 231 | {children} 232 | 233 | 234 | ); 235 | }; 236 | 237 | export const useCopilot = () => { 238 | const value = useContext(CopilotContext); 239 | if (value == null) { 240 | throw new Error("You must wrap your app inside CopilotProvider"); 241 | } 242 | 243 | return value; 244 | }; 245 | -------------------------------------------------------------------------------- /src/hocs/copilot.tsx: -------------------------------------------------------------------------------- 1 | import React, { type FunctionComponent, type ComponentType } from "react"; 2 | import { CopilotProvider, useCopilot } from "../contexts/CopilotProvider"; 3 | import { type CopilotOptions } from "../types"; 4 | 5 | const ComponentWithCopilotContext = (WrappedComponent: ComponentType) => { 6 | const Component: FunctionComponent = (props) => { 7 | const copilot = useCopilot(); 8 | return ; 9 | }; 10 | 11 | Component.displayName = `CopilotInjector(${ 12 | WrappedComponent.displayName ?? WrappedComponent.name ?? "Component" 13 | })`; 14 | 15 | return Component; 16 | }; 17 | 18 | /** 19 | * @deprecated The HOC is deprecated. Please use `CopilotProvider` instead. 20 | */ 21 | export function copilot

(options: CopilotOptions) { 22 | return (WrappedComponent: ComponentType) => { 23 | const OuterComponent: FunctionComponent

= (props) => { 24 | const InnerComponentWithCopilotContext = 25 | ComponentWithCopilotContext(WrappedComponent); 26 | 27 | return ( 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | OuterComponent.displayName = `copilot(${ 35 | WrappedComponent.displayName ?? WrappedComponent.name ?? "Component" 36 | })`; 37 | 38 | return OuterComponent; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/hocs/tests/walkthroughable.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Text, ScrollView, TextInput } from "react-native"; 3 | import renderer from "react-test-renderer"; 4 | import { walkthroughable } from "../walkthroughable"; 5 | 6 | const WalkthroughableView = walkthroughable(View); 7 | const WalkthroughableText = walkthroughable(Text); 8 | const WalkthroughableScrollView = walkthroughable(ScrollView); 9 | const WalkthroughableTextInput = walkthroughable(TextInput); 10 | 11 | const walkthroughableComponents = [ 12 | WalkthroughableView, 13 | WalkthroughableText, 14 | WalkthroughableScrollView, 15 | WalkthroughableTextInput, 16 | ]; 17 | 18 | const nativeComponents = [View, Text, ScrollView, TextInput]; 19 | 20 | it("spreads the copilot prop object on the wrapped component", () => { 21 | const tree = renderer.create( 22 | // @ts-expect-error just for testing 23 | 24 | ); 25 | 26 | const { props } = tree.root.findByType(View as any ); 27 | 28 | expect(props.keyForNum).toBe(1); 29 | expect(props.keyForStr).toBe("hello"); 30 | }); 31 | 32 | it("spreads the copilot prop object on the wrapped component along with other flat props", () => { 33 | const tree = renderer.create( 34 | 39 | ); 40 | 41 | const { props } = tree.root.findByType(View as any ); 42 | 43 | expect(props.keyForNum).toBe(1); 44 | expect(props.keyForStr).toBe("hello"); 45 | expect(props.otherProp).toBe("the other prop"); 46 | }); 47 | 48 | it("spreads the copilot prop object on the wrapped component not overriding the root props", () => { 49 | const tree = renderer.create( 50 | 55 | ); 56 | 57 | const { props } = tree.root.findByType(View as any ); 58 | 59 | expect(props.keyForNum).toBe(2); 60 | expect(props.keyForStr).toBe("hello"); 61 | }); 62 | 63 | it("works with all types of react native built-in components", () => { 64 | nativeComponents.forEach((Component, key) => { 65 | const WalkthroughableComponent = walkthroughableComponents[key]; 66 | 67 | const tree = renderer.create( 68 | 72 | ); 73 | 74 | const { props } = tree.root.findByType(Component as any ); 75 | 76 | expect(props.keyForNum).toBe(1); 77 | expect(props.keyForStr).toBe("hello"); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/hocs/walkthroughable.tsx: -------------------------------------------------------------------------------- 1 | import React, { type FunctionComponent } from "react"; 2 | import { type NativeMethods } from "react-native/types"; 3 | 4 | interface CopilotType { 5 | ref?: React.RefObject; 6 | onLayout?: () => void; 7 | } 8 | 9 | export function walkthroughable

( 10 | WrappedComponent: React.ComponentType

, 11 | ) { 12 | const Component: FunctionComponent

= (props: P) => { 13 | const { copilot, ...rest } = props as { copilot: CopilotType } & P; 14 | return ; 15 | }; 16 | 17 | Component.displayName = "Walkthroughable"; 18 | 19 | return Component; 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/useStateWithAwait.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | /** 4 | * A hook like useState that allows you to use await the setter 5 | */ 6 | export const useStateWithAwait = ( 7 | initialState: T 8 | ): [T, (newValue: T) => Promise] => { 9 | const endPending = useRef(() => {}); 10 | const newDesiredValue = useRef(initialState); 11 | 12 | const [state, setState] = useState(initialState); 13 | 14 | const setStateWithAwait = async (newState: T) => { 15 | const pending = new Promise((resolve) => { 16 | endPending.current = resolve; 17 | }); 18 | newDesiredValue.current = newState; 19 | setState(newState); 20 | await pending; 21 | }; 22 | 23 | useEffect(() => { 24 | if (state === newDesiredValue.current) { 25 | endPending.current(); 26 | } 27 | }, [state]); 28 | 29 | return [state, setStateWithAwait]; 30 | }; 31 | -------------------------------------------------------------------------------- /src/hooks/useStepsMap.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useReducer, useState } from "react"; 2 | import { type Step, type StepsMap } from "../types"; 3 | 4 | type Action = 5 | | { 6 | type: "register"; 7 | step: Step; 8 | } 9 | | { 10 | type: "unregister"; 11 | stepName: string; 12 | }; 13 | 14 | export const useStepsMap = () => { 15 | const [currentStep, setCurrentStepState] = useState( 16 | undefined 17 | ); 18 | const [steps, dispatch] = useReducer((state: StepsMap, action: Action) => { 19 | switch (action.type) { 20 | case "register": 21 | return { 22 | ...state, 23 | [action.step.name]: action.step, 24 | }; 25 | case "unregister": { 26 | const { [action.stepName]: _, ...rest } = state; 27 | return rest; 28 | } 29 | default: 30 | return state; 31 | } 32 | }, {}); 33 | 34 | const orderedSteps = useMemo( 35 | () => Object.values(steps).sort((a, b) => a.order - b.order), 36 | [steps] 37 | ); 38 | 39 | const stepIndex = useCallback( 40 | (step = currentStep) => 41 | step 42 | ? orderedSteps.findIndex( 43 | (stepCandidate) => stepCandidate.order === step.order 44 | ) 45 | : -1, 46 | [currentStep, orderedSteps] 47 | ); 48 | 49 | const currentStepNumber = useMemo( 50 | (step = currentStep) => stepIndex(step) + 1, 51 | [currentStep, stepIndex] 52 | ); 53 | 54 | const totalStepsNumber = useMemo(() => orderedSteps.length, [orderedSteps]); 55 | 56 | const getFirstStep = useCallback(() => orderedSteps[0], [orderedSteps]); 57 | 58 | const getLastStep = useCallback( 59 | () => orderedSteps[orderedSteps.length - 1], 60 | [orderedSteps] 61 | ); 62 | 63 | const getPrevStep = useCallback( 64 | (step = currentStep) => step && orderedSteps[stepIndex(step) - 1], 65 | [currentStep, stepIndex, orderedSteps] 66 | ); 67 | 68 | const getNextStep = useCallback( 69 | (step = currentStep) => step && orderedSteps[stepIndex(step) + 1], 70 | [currentStep, stepIndex, orderedSteps] 71 | ); 72 | 73 | const getNthStep = useCallback( 74 | (n: number) => orderedSteps[n - 1], 75 | [orderedSteps] 76 | ); 77 | 78 | const isFirstStep = useMemo( 79 | () => currentStep === getFirstStep(), 80 | [currentStep, getFirstStep] 81 | ); 82 | 83 | const isLastStep = useMemo( 84 | () => currentStep === getLastStep(), 85 | [currentStep, getLastStep] 86 | ); 87 | 88 | const registerStep = useCallback((step: Step) => { 89 | dispatch({ type: "register", step }); 90 | }, []); 91 | 92 | const unregisterStep = useCallback((stepName: string) => { 93 | dispatch({ type: "unregister", stepName }); 94 | }, []); 95 | 96 | return { 97 | currentStepNumber, 98 | totalStepsNumber, 99 | getFirstStep, 100 | getLastStep, 101 | getPrevStep, 102 | getNextStep, 103 | getNthStep, 104 | isFirstStep, 105 | isLastStep, 106 | currentStep, 107 | setCurrentStepState, 108 | steps, 109 | registerStep, 110 | unregisterStep, 111 | }; 112 | }; 113 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { StepNumber } from "./components/default-ui/StepNumber"; 2 | import { Tooltip } from "./components/default-ui/Tooltip"; 3 | export { walkthroughable } from "./hocs/walkthroughable"; 4 | export { CopilotStep } from "./components/CopilotStep"; 5 | export { CopilotProvider, useCopilot } from "./contexts/CopilotProvider"; 6 | export type { CopilotOptions as CopilotProps, TooltipProps } from "./types"; 7 | 8 | export const DefaultUI = { 9 | StepNumber, 10 | Tooltip, 11 | }; 12 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Animated, 3 | LayoutRectangle, 4 | NativeMethods, 5 | ViewStyle, 6 | } from "react-native"; 7 | 8 | export type WalktroughedComponent = NativeMethods & React.ComponentType; 9 | 10 | export interface Step { 11 | name: string; 12 | order: number; 13 | visible: boolean; 14 | wrapperRef: React.RefObject; 15 | measure: () => Promise; 16 | text: string; 17 | } 18 | 19 | export interface CopilotContext { 20 | registerStep: (step: Step) => void; 21 | unregisterStep: (name: string) => void; 22 | getCurrentStep: () => Step | undefined; 23 | } 24 | 25 | export interface ValueXY { 26 | x: number; 27 | y: number; 28 | } 29 | 30 | export type SvgMaskPathFunction = (args: { 31 | size: Animated.ValueXY; 32 | position: Animated.ValueXY; 33 | canvasSize: ValueXY; 34 | step: Step; 35 | }) => string; 36 | 37 | export type StepsMap = Record; 38 | 39 | export type EasingFunction = (value: number) => number; 40 | 41 | export type Labels = Partial< 42 | Record<"skip" | "previous" | "next" | "finish", string> 43 | >; 44 | 45 | export interface TooltipProps { 46 | labels: Labels; 47 | } 48 | 49 | export interface MaskProps { 50 | size: ValueXY; 51 | position: ValueXY; 52 | style: ViewStyle; 53 | easing?: EasingFunction; 54 | animationDuration: number; 55 | animated: boolean; 56 | backdropColor: string; 57 | svgMaskPath?: SvgMaskPathFunction; 58 | layout: { 59 | width: number; 60 | height: number; 61 | }; 62 | onClick?: () => any; 63 | currentStep: Step; 64 | } 65 | 66 | export interface CopilotOptions { 67 | easing?: ((value: number) => number) | undefined; 68 | overlay?: "svg" | "view"; 69 | animationDuration?: number; 70 | tooltipComponent?: React.ComponentType; 71 | tooltipStyle?: ViewStyle; 72 | stepNumberComponent?: React.ComponentType; 73 | animated?: boolean; 74 | labels?: Labels; 75 | androidStatusBarVisible?: boolean; 76 | svgMaskPath?: SvgMaskPathFunction; 77 | verticalOffset?: number; 78 | arrowColor?: string; 79 | arrowSize?: number 80 | margin?: number 81 | stopOnOutsideClick?: boolean; 82 | backdropColor?: string; 83 | } 84 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/react-native/tsconfig.json", 3 | "exclude": ["dist"], 4 | "include": ["src", "./tsup.config.ts"], 5 | "compilerOptions": { 6 | "types": ["react-native", "jest", "node"], 7 | "target": "ES2015" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | import fs from "fs/promises"; 3 | import path from "path"; 4 | 5 | export default defineConfig({ 6 | entry: ["src/index.ts"], 7 | format: ["cjs", "esm"], 8 | sourcemap: true, 9 | dts: true, 10 | external: ["react", "react-native", "react-native-svg"], 11 | platform: "neutral", 12 | async onSuccess() { 13 | if (process.env.NODE_ENV === "development") { 14 | const exampleOutputPath = path.resolve( 15 | "./example/node_modules/react-native-copilot" 16 | ); 17 | const exampleOutputNodeModulesPath = path.resolve( 18 | exampleOutputPath, 19 | "node_modules" 20 | ); 21 | 22 | await Promise.all( 23 | ["dist/index.js", "dist/index.d.ts"].map(async (file) => { 24 | const outputPath = path.resolve(exampleOutputPath, file); 25 | console.log("Copying file: ", file, "to ->", outputPath); 26 | await fs.copyFile(file, outputPath); 27 | }) 28 | ); 29 | await fs.rm(exampleOutputNodeModulesPath, { 30 | recursive: true, 31 | force: true, 32 | }); 33 | console.log("Copied files to example project"); 34 | } 35 | }, 36 | }); 37 | --------------------------------------------------------------------------------