├── .editorconfig ├── .gitattributes ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .watchmanconfig ├── .yarnrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── example ├── .gitignore ├── App.js ├── app.json ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── metro.config.js ├── package.json ├── src │ └── App.tsx ├── tsconfig.json ├── webpack.config.js └── yarn.lock ├── lefthook.yml ├── package-lock.json ├── package.json ├── scripts └── bootstrap.js ├── src ├── __tests__ │ └── index.test.tsx ├── contexts │ ├── AuthContext.ts │ └── AuthProvider.tsx ├── hooks │ └── useAuth.ts ├── index.tsx ├── interfaces │ ├── AuthConfig.ts │ ├── AuthContextType.ts │ ├── AuthProviderProps.ts │ ├── AuthResponse.ts │ └── User.ts ├── services │ └── AuthService.ts └── utils │ ├── TokenStorage.native.ts │ └── TokenStorage.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@v3 9 | with: 10 | node-version-file: .nvmrc 11 | 12 | - name: Cache dependencies 13 | id: yarn-cache 14 | uses: actions/cache@v3 15 | with: 16 | path: | 17 | **/node_modules 18 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }} 19 | restore-keys: | 20 | ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 21 | ${{ runner.os }}-yarn- 22 | 23 | - name: Install dependencies 24 | if: steps.yarn-cache.outputs.cache-hit != 'true' 25 | run: | 26 | yarn install --cwd example --frozen-lockfile 27 | yarn install --frozen-lockfile 28 | shell: bash 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup 18 | uses: ./.github/actions/setup 19 | 20 | - name: Lint files 21 | run: yarn lint 22 | 23 | - name: Typecheck files 24 | run: yarn typecheck 25 | 26 | test: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | 32 | - name: Setup 33 | uses: ./.github/actions/setup 34 | 35 | - name: Run unit tests 36 | run: yarn test --maxWorkers=2 --coverage 37 | 38 | build-library: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v3 43 | 44 | - name: Setup 45 | uses: ./.github/actions/setup 46 | 47 | - name: Build package 48 | run: yarn prepack 49 | 50 | build-web: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Checkout 54 | uses: actions/checkout@v3 55 | 56 | - name: Setup 57 | uses: ./.github/actions/setup 58 | 59 | - name: Build example for Web 60 | run: | 61 | yarn example expo export:web 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | 47 | # Ruby 48 | example/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Expo 64 | .expo/ 65 | 66 | # Turborepo 67 | .turbo/ 68 | 69 | # generated by bob 70 | lib/ 71 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # Override Yarn command so we can automatically setup the repo on running `yarn` 2 | 3 | yarn-path "scripts/bootstrap.js" 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | info@marcelwagner.dev. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome, no matter how large or small! 4 | 5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). 6 | 7 | ## Development workflow 8 | 9 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: 10 | 11 | ```sh 12 | yarn 13 | ``` 14 | 15 | > While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development. 16 | 17 | While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. 18 | 19 | To start the packager: 20 | 21 | ```sh 22 | yarn example start 23 | ``` 24 | 25 | To run the example app on Android: 26 | 27 | ```sh 28 | yarn example android 29 | ``` 30 | 31 | To run the example app on iOS: 32 | 33 | ```sh 34 | yarn example ios 35 | ``` 36 | 37 | To run the example app on Web: 38 | 39 | ```sh 40 | yarn example web 41 | ``` 42 | 43 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 44 | 45 | ```sh 46 | yarn typecheck 47 | yarn lint 48 | ``` 49 | 50 | To fix formatting errors, run the following: 51 | 52 | ```sh 53 | yarn lint --fix 54 | ``` 55 | 56 | Remember to add tests for your change if possible. Run the unit tests by: 57 | 58 | ```sh 59 | yarn test 60 | ``` 61 | 62 | 63 | ### Commit message convention 64 | 65 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 66 | 67 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 68 | - `feat`: new features, e.g. add new method to the module. 69 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 70 | - `docs`: changes into documentation, e.g. add usage example for the module.. 71 | - `test`: adding or updating tests, e.g. add integration tests using detox. 72 | - `chore`: tooling changes, e.g. change CI config. 73 | 74 | Our pre-commit hooks verify that your commit message matches this format when committing. 75 | 76 | ### Linting and tests 77 | 78 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 79 | 80 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 81 | 82 | Our pre-commit hooks verify that the linter and tests pass when committing. 83 | 84 | ### Publishing to npm 85 | 86 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. 87 | 88 | To publish new versions, run the following: 89 | 90 | ```sh 91 | yarn release 92 | ``` 93 | 94 | ### Scripts 95 | 96 | The `package.json` file contains various scripts for common tasks: 97 | 98 | - `yarn bootstrap`: setup project by installing all dependencies and pods. 99 | - `yarn typecheck`: type-check files with TypeScript. 100 | - `yarn lint`: lint files with ESLint. 101 | - `yarn test`: run unit tests with Jest. 102 | - `yarn example start`: start the Metro server for the example app. 103 | - `yarn example android`: run the example app on Android. 104 | - `yarn example ios`: run the example app on iOS. 105 | 106 | ### Sending a pull request 107 | 108 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 109 | 110 | When you're sending a pull request: 111 | 112 | - Prefer small pull requests focused on one change. 113 | - Verify that linters and tests are passing. 114 | - Review the documentation to make sure it looks good. 115 | - Follow the pull request template when opening a pull request. 116 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Marcel Wagner 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Laravel Sanctum 2 | 3 | React Native auth package for [Laravel Sanctum ](https://laravel.com/docs/10.x/sanctum) integration. While this package is mainly build for Laravel Sanctum APIs, you can use this library for other API-Backends as well. 4 | 5 | ## Table of Contents 6 | 7 | - [Features](#features) 8 | - [Installation](#installation) 9 | - [Basic Usage](#basic-usage) 10 | - [Example App](#example-app) 11 | - [Documentation](#documentation) 12 | - [Contributing](#contributing) 13 | - [License](#license) 14 | 15 | ## Features 16 | 17 | - [x] Login 18 | - [x] Logout 19 | - [x] Get current user 20 | - [x] Check if user is authenticated 21 | - [x] CSRF-Token 22 | 23 | ## Installation 24 | 25 | ```sh 26 | npm install react-native-laravel-sanctum 27 | ``` 28 | ### Install native dependencies 29 | As the package relies on `expo-secure-store` you will need to create a new build of your app to use it. 30 | 31 | ## Basic Usage 32 | 33 | Wrap your app with `AuthProvider` and pass the `config` object as a prop. 34 | 35 | Make sure your validation rules, within your controller look like this: 36 | ```php 37 | [ 38 | 'email' => 'required|email', 39 | 'password' => 'required', 40 | 'deviceName' => 'required', 41 | ]; 42 | ``` 43 | 44 | ```js 45 | import { AuthProvider } from 'react-native-laravel-sanctum'; 46 | 47 | 48 | export default function App() { 49 | const config = { 50 | loginUrl: 'https://your-awesome-domain/api/sanctum/token', 51 | logoutUrl: 'https://your-awesome-domain/api/logout', 52 | userUrl: 'https://your-awesome-domain/api/user', 53 | csrfTokenUrl: 'https://your-awesome-domain/sanctum/csrf-cookie' 54 | }; 55 | 56 | return ( 57 | 58 | 59 | ... 60 | 61 | 62 | ); 63 | } 64 | ``` 65 | 66 | You can now use the `useAuth` hook within the AuthProvider to access the auth methods. 67 | 68 | ```js 69 | import { useAuth } from 'react-native-laravel-sanctum'; 70 | 71 | export default function Login() { 72 | const { login, getToken, updateUser, setUserIsAuthenticated, isAuthenticated, logout, currentUser } = useAuth(); 73 | const [email, setEmail] = useState(''); 74 | const [password, setPassword] = useState(''); 75 | 76 | const handleLogin = async () => { 77 | const tokenObtained = await login(email, password, 'Test-Device'); 78 | if (tokenObtained) { 79 | const user = await updateUser(); 80 | 81 | if (user.id) { 82 | setUserIsAuthenticated(true); 83 | } 84 | } 85 | }; 86 | 87 | return ( 88 | 89 | You are not logged in. 90 | setEmail(text)} 92 | value={email} 93 | placeholder='Email' 94 | /> 95 | setPassword(text)} 97 | value={password} 98 | secureTextEntry 99 | placeholder='Password' 100 | /> 101 | 102 | Login 103 | 104 | 105 | ); 106 | } 107 | ``` 108 | 109 | ## Example App 110 | 111 | You can find the example app [here](https://github.com/Tschucki/react-native-laravel-sanctum-example-app). 112 | 113 | ## Documentation 114 | 115 | You can find the documentation [here](https://github.com/Tschucki/react-native-laravel-sanctum/wiki). 116 | 117 | ## Contributing 118 | 119 | See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. 120 | 121 | ## License 122 | 123 | MIT 124 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | web-build 2 | -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | export { default } from './src/App'; 2 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-laravel-sanctum-example", 4 | "slug": "react-native-laravel-sanctum-example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./assets/adaptive-icon.png", 23 | "backgroundColor": "#ffffff" 24 | } 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tschucki/react-native-laravel-sanctum/674df4d8075f9fc7c223b8040d550ae9503e33de/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tschucki/react-native-laravel-sanctum/674df4d8075f9fc7c223b8040d550ae9503e33de/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tschucki/react-native-laravel-sanctum/674df4d8075f9fc7c223b8040d550ae9503e33de/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tschucki/react-native-laravel-sanctum/674df4d8075f9fc7c223b8040d550ae9503e33de/example/assets/splash.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = function (api) { 5 | api.cache(true); 6 | 7 | return { 8 | presets: ['babel-preset-expo'], 9 | plugins: [ 10 | [ 11 | 'module-resolver', 12 | { 13 | extensions: ['.tsx', '.ts', '.js', '.json'], 14 | alias: { 15 | // For development, we want to alias the library to the source 16 | [pak.name]: path.join(__dirname, '..', pak.source), 17 | }, 18 | }, 19 | ], 20 | ], 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const escape = require('escape-string-regexp'); 3 | const { getDefaultConfig } = require('@expo/metro-config'); 4 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 5 | const pak = require('../package.json'); 6 | 7 | const root = path.resolve(__dirname, '..'); 8 | const modules = Object.keys({ ...pak.peerDependencies }); 9 | 10 | const defaultConfig = getDefaultConfig(__dirname); 11 | 12 | /** 13 | * Metro configuration 14 | * https://facebook.github.io/metro/docs/configuration 15 | * 16 | * @type {import('metro-config').MetroConfig} 17 | */ 18 | const config = { 19 | ...defaultConfig, 20 | 21 | projectRoot: __dirname, 22 | watchFolders: [root], 23 | 24 | // We need to make sure that only one version is loaded for peerDependencies 25 | // So we block them at the root, and alias them to the versions in example's node_modules 26 | resolver: { 27 | ...defaultConfig.resolver, 28 | 29 | blacklistRE: exclusionList( 30 | modules.map( 31 | (m) => 32 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 33 | ) 34 | ), 35 | 36 | extraNodeModules: modules.reduce((acc, name) => { 37 | acc[name] = path.join(__dirname, 'node_modules', name); 38 | return acc; 39 | }, {}), 40 | }, 41 | }; 42 | 43 | module.exports = config; 44 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "expo": "~49.0.7", 13 | "expo-secure-store": "~12.3.1", 14 | "expo-status-bar": "~1.6.0", 15 | "react": "18.2.0", 16 | "react-dom": "18.2.0", 17 | "react-native": "0.72.4", 18 | "react-native-laravel-sanctum": "^0.3.0", 19 | "react-native-web": "~0.19.6" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.20.0", 23 | "@expo/webpack-config": "^19.0.0", 24 | "babel-loader": "^8.1.0", 25 | "babel-plugin-module-resolver": "^5.0.0" 26 | }, 27 | "private": true 28 | } 29 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; 3 | import { AuthProvider, useAuth } from 'react-native-laravel-sanctum'; 4 | 5 | function Login() { 6 | const { 7 | login, 8 | getToken, 9 | updateUser, 10 | setUserIsAuthenticated, 11 | isAuthenticated, 12 | currentUser, 13 | } = useAuth(); 14 | const [email, setEmail] = useState(''); 15 | const [password, setPassword] = useState(''); 16 | 17 | useEffect(() => { 18 | console.log(currentUser); 19 | }, [currentUser]); 20 | useEffect(() => { 21 | console.log(isAuthenticated); 22 | }, [isAuthenticated]); 23 | 24 | const handleLogin = async () => { 25 | console.log(await getToken()); 26 | const tokenObtained = await login(email, password, 'Test-Device'); 27 | console.log(tokenObtained); 28 | if (tokenObtained) { 29 | const user = await updateUser(); 30 | if (user && user.id) { 31 | setUserIsAuthenticated(true); 32 | } 33 | } 34 | }; 35 | 36 | return ( 37 | 38 | You are not logged in. 39 | setEmail(text)} 42 | value={email} 43 | placeholder="Email" 44 | /> 45 | setPassword(text)} 48 | value={password} 49 | secureTextEntry 50 | placeholder="Password" 51 | /> 52 | 53 | Login 54 | 55 | 56 | ); 57 | } 58 | 59 | function Logout() { 60 | const { logout, setUserIsAuthenticated, updateUser, currentUser } = useAuth(); 61 | 62 | const handleLogout = async () => { 63 | const isLoggedOut = await logout(); 64 | if (isLoggedOut) { 65 | setUserIsAuthenticated(false); 66 | await updateUser(); 67 | } 68 | }; 69 | 70 | return ( 71 | 72 | 73 | You are logged in as {currentUser ? currentUser.name : ''} 74 | 75 | 76 | Logout 77 | 78 | 79 | ); 80 | } 81 | 82 | function Example() { 83 | const { isAuthenticated } = useAuth(); 84 | return ( 85 | {isAuthenticated ? : } 86 | ); 87 | } 88 | 89 | export default function App() { 90 | const config = { 91 | loginUrl: 'http://127.0.0.1:8000/api/sanctum/token', 92 | logoutUrl: 'http://127.0.0.1:8000/api/logout', 93 | userUrl: 'http://127.0.0.1:8000/api/user', 94 | }; 95 | 96 | return ( 97 | 98 | 99 | 100 | 101 | 102 | ); 103 | } 104 | 105 | const styles = StyleSheet.create({ 106 | container: { 107 | flex: 1, 108 | backgroundColor: '#fff', 109 | alignItems: 'center', 110 | justifyContent: 'center', 111 | }, 112 | button: { 113 | backgroundColor: '#f59e0b', 114 | padding: 12, 115 | minWidth: '80%', 116 | borderRadius: 5, 117 | alignItems: 'center', 118 | }, 119 | buttonText: { 120 | fontWeight: '800', 121 | letterSpacing: 0.5, 122 | lineHeight: 20, 123 | color: '#fff', 124 | }, 125 | textInput: { 126 | height: 40, 127 | borderColor: 'gray', 128 | borderWidth: 1, 129 | minWidth: '80%', 130 | padding: 10, 131 | borderRadius: 5, 132 | marginBottom: 10, 133 | }, 134 | caption: { 135 | marginBottom: 10, 136 | fontWeight: '800', 137 | letterSpacing: 0.5, 138 | lineHeight: 20, 139 | color: '#000', 140 | textAlign: 'center', 141 | }, 142 | view: { 143 | padding: 15, 144 | alignItems: 'center', 145 | }, 146 | }); 147 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | // Avoid expo-cli auto-generating a tsconfig 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config'); 3 | const { resolver } = require('./metro.config'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const node_modules = path.join(__dirname, 'node_modules'); 7 | 8 | module.exports = async function (env, argv) { 9 | const config = await createExpoWebpackConfigAsync(env, argv); 10 | 11 | config.module.rules.push({ 12 | test: /\.(js|jsx|ts|tsx)$/, 13 | include: path.resolve(root, 'src'), 14 | use: 'babel-loader', 15 | }); 16 | 17 | // We need to make sure that only one version is loaded for peerDependencies 18 | // So we alias them to the versions in example's node_modules 19 | Object.assign(config.resolve.alias, { 20 | ...resolver.extraNodeModules, 21 | 'react-native-web': path.join(node_modules, 'react-native-web'), 22 | }); 23 | 24 | return config; 25 | }; 26 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | files: git diff --name-only @{push} 6 | glob: "*.{js,ts,jsx,tsx}" 7 | run: npx eslint {files} 8 | types: 9 | files: git diff --name-only @{push} 10 | glob: "*.{js,ts, jsx, tsx}" 11 | run: npx tsc --noEmit 12 | commit-msg: 13 | parallel: true 14 | commands: 15 | commitlint: 16 | run: npx commitlint --edit 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-laravel-sanctum", 3 | "version": "0.3.0", 4 | "description": "React Native auth package for Laravel Sanctum integration", 5 | "main": "lib/commonjs/index", 6 | "module": "lib/module/index", 7 | "types": "lib/typescript/index.d.ts", 8 | "react-native": "src/index", 9 | "source": "src/index", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "android", 14 | "ios", 15 | "cpp", 16 | "*.podspec", 17 | "!lib/typescript/example", 18 | "!ios/build", 19 | "!android/build", 20 | "!android/gradle", 21 | "!android/gradlew", 22 | "!android/gradlew.bat", 23 | "!android/local.properties", 24 | "!**/__tests__", 25 | "!**/__fixtures__", 26 | "!**/__mocks__", 27 | "!**/.*" 28 | ], 29 | "scripts": { 30 | "test": "jest", 31 | "typecheck": "tsc --noEmit", 32 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 33 | "prepack": "bob build", 34 | "example": "yarn --cwd example", 35 | "release": "release-it" 36 | }, 37 | "keywords": [ 38 | "react-native", 39 | "ios", 40 | "android" 41 | ], 42 | "repository": "https://github.com/Tschucki/react-native-laravel-sanctum", 43 | "author": "Marcel Wagner (https://www.marcelwagner.dev)", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/Tschucki/react-native-laravel-sanctum/issues" 47 | }, 48 | "homepage": "https://github.com/Tschucki/react-native-laravel-sanctum#readme", 49 | "publishConfig": { 50 | "registry": "https://registry.npmjs.org/" 51 | }, 52 | "devDependencies": { 53 | "@commitlint/config-conventional": "^17.0.2", 54 | "@evilmartians/lefthook": "^1.2.2", 55 | "@react-native-community/eslint-config": "^3.0.2", 56 | "@release-it/conventional-changelog": "^5.0.0", 57 | "@types/jest": "^28.1.2", 58 | "@types/react": "~17.0.21", 59 | "@types/react-native": "0.70.0", 60 | "commitlint": "^17.0.2", 61 | "del-cli": "^5.0.0", 62 | "eslint": "^8.4.1", 63 | "eslint-config-prettier": "^8.5.0", 64 | "eslint-plugin-prettier": "^4.0.0", 65 | "jest": "^28.1.1", 66 | "pod-install": "^0.1.0", 67 | "prettier": "^2.0.5", 68 | "react": "18.2.0", 69 | "react-native": "0.72.3", 70 | "react-native-builder-bob": "^0.21.3", 71 | "release-it": "^15.0.0", 72 | "typescript": "^5.0.2" 73 | }, 74 | "resolutions": { 75 | "@types/react": "17.0.21" 76 | }, 77 | "peerDependencies": { 78 | "react": "*", 79 | "react-native": "*" 80 | }, 81 | "engines": { 82 | "node": ">= 16.0.0" 83 | }, 84 | "jest": { 85 | "preset": "react-native", 86 | "modulePathIgnorePatterns": [ 87 | "/example/node_modules", 88 | "/lib/" 89 | ] 90 | }, 91 | "commitlint": { 92 | "extends": [ 93 | "@commitlint/config-conventional" 94 | ] 95 | }, 96 | "release-it": { 97 | "git": { 98 | "commitMessage": "chore: release ${version}", 99 | "tagName": "v${version}" 100 | }, 101 | "npm": { 102 | "publish": true 103 | }, 104 | "github": { 105 | "release": true 106 | }, 107 | "plugins": { 108 | "@release-it/conventional-changelog": { 109 | "preset": "angular" 110 | } 111 | } 112 | }, 113 | "eslintConfig": { 114 | "root": true, 115 | "extends": [ 116 | "@react-native-community", 117 | "prettier" 118 | ], 119 | "rules": { 120 | "prettier/prettier": [ 121 | "error", 122 | { 123 | "quoteProps": "consistent", 124 | "singleQuote": true, 125 | "tabWidth": 2, 126 | "trailingComma": "es5", 127 | "useTabs": false 128 | } 129 | ] 130 | } 131 | }, 132 | "eslintIgnore": [ 133 | "node_modules/", 134 | "lib/" 135 | ], 136 | "prettier": { 137 | "quoteProps": "consistent", 138 | "singleQuote": true, 139 | "tabWidth": 2, 140 | "trailingComma": "es5", 141 | "useTabs": false 142 | }, 143 | "react-native-builder-bob": { 144 | "source": "src", 145 | "output": "lib", 146 | "targets": [ 147 | "commonjs", 148 | "module", 149 | [ 150 | "typescript", 151 | { 152 | "project": "tsconfig.build.json" 153 | } 154 | ] 155 | ] 156 | }, 157 | "dependencies": { 158 | "@react-native-async-storage/async-storage": "^1.21.0", 159 | "expo-secure-store": "^12.3.1" 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const child_process = require('child_process'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const args = process.argv.slice(2); 7 | const options = { 8 | cwd: process.cwd(), 9 | env: process.env, 10 | stdio: 'inherit', 11 | encoding: 'utf-8', 12 | }; 13 | 14 | if (os.type() === 'Windows_NT') { 15 | options.shell = true; 16 | } 17 | 18 | let result; 19 | 20 | if (process.cwd() !== root || args.length) { 21 | // We're not in the root of the project, or additional arguments were passed 22 | // In this case, forward the command to `yarn` 23 | result = child_process.spawnSync('yarn', args, options); 24 | } else { 25 | // If `yarn` is run without arguments, perform bootstrap 26 | result = child_process.spawnSync('yarn', ['bootstrap'], options); 27 | } 28 | 29 | process.exitCode = result.status; 30 | -------------------------------------------------------------------------------- /src/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | it.todo('write a test'); 2 | -------------------------------------------------------------------------------- /src/contexts/AuthContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import type { AuthContextType } from '../interfaces/AuthContextType'; 3 | 4 | const AuthContext = createContext(undefined); 5 | 6 | export default AuthContext; 7 | -------------------------------------------------------------------------------- /src/contexts/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import type { AuthConfig } from '../interfaces/AuthConfig'; 3 | import AuthContext from './AuthContext'; 4 | import type { AuthProviderProps } from '../interfaces/AuthProviderProps'; 5 | import type { User } from '../interfaces/User'; 6 | import AuthService from '../services/AuthService'; 7 | import TokenStorage from '../utils/TokenStorage'; 8 | 9 | const AuthProvider: React.FC = ({ config, children }) => { 10 | const [currentConfig, setCurrentConfig] = useState(config); 11 | const [currentUser, setCurrentUser] = useState(null); 12 | const [isAuthenticated, setIsAuthenticated] = useState(false); 13 | const [authService, setAuthService] = useState(); 14 | 15 | useEffect(() => { 16 | if (authService) { 17 | checkIfAuthenticated(); 18 | } 19 | // eslint-disable-next-line react-hooks/exhaustive-deps 20 | }, [authService]); 21 | 22 | useEffect(() => { 23 | return setAuthService(new AuthService(currentConfig)); 24 | }, [currentConfig]); 25 | 26 | useEffect(() => { 27 | setCurrentConfig(config); 28 | }, [config]); 29 | 30 | const checkIfAuthenticated = async () => { 31 | if (authService === undefined) throw new Error('AuthService is undefined'); 32 | try { 33 | await updateUser().then((user) => { 34 | if (user === null) { 35 | setUserIsAuthenticated(false); 36 | } else if (user.id) { 37 | setUserIsAuthenticated(true); 38 | } 39 | }); 40 | } catch (error) { 41 | console.error('Error while checking token:', error); 42 | throw error; 43 | } 44 | }; 45 | 46 | const login = async (email: string, password: string, deviceName: string) => { 47 | if (authService === undefined) throw new Error('AuthService is undefined'); 48 | try { 49 | return await authService.login(email, password, deviceName); 50 | } catch (error) { 51 | console.error('Error during login:', error); 52 | throw error; 53 | } 54 | }; 55 | 56 | const updateUser = async (): Promise => { 57 | if (authService === undefined) throw new Error('AuthService is undefined'); 58 | try { 59 | return await authService.getUser().then((fetchedUser) => { 60 | setCurrentUser(fetchedUser); 61 | return fetchedUser; 62 | }); 63 | } catch (error) { 64 | console.error('Error while fetching user:', error); 65 | throw error; 66 | } 67 | }; 68 | 69 | const setUserIsAuthenticated = (userIsAuthenticated: boolean): void => { 70 | setIsAuthenticated(userIsAuthenticated); 71 | }; 72 | 73 | const getToken = async () => { 74 | return await TokenStorage.getToken() 75 | .then((apiToken) => { 76 | return apiToken; 77 | }) 78 | .catch((error) => { 79 | console.error('Error while getting token:', error); 80 | throw error; 81 | }) 82 | .finally(() => { 83 | return null; 84 | }); 85 | }; 86 | 87 | const logout = async () => { 88 | if (authService === undefined) throw new Error('AuthService is undefined'); 89 | try { 90 | return await authService.logout(); 91 | } catch (error) { 92 | console.error('Error during logout:', error); 93 | throw error; 94 | } 95 | }; 96 | 97 | return ( 98 | 109 | {children} 110 | 111 | ); 112 | }; 113 | 114 | export { AuthProvider }; 115 | -------------------------------------------------------------------------------- /src/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import AuthContext from '../contexts/AuthContext'; 3 | 4 | const useAuth = () => { 5 | const context = useContext(AuthContext); 6 | if (!context) { 7 | throw new Error('useAuth muss innerhalb von AuthProvider verwendet werden'); 8 | } 9 | return context; 10 | }; 11 | 12 | export { useAuth }; 13 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { AuthProvider } from './contexts/AuthProvider'; 2 | export type { User } from './interfaces/User'; 3 | export type { AuthConfig } from './interfaces/AuthConfig'; 4 | export { useAuth } from './hooks/useAuth'; 5 | -------------------------------------------------------------------------------- /src/interfaces/AuthConfig.ts: -------------------------------------------------------------------------------- 1 | export interface AuthConfig { 2 | loginUrl: string; 3 | logoutUrl: string; 4 | userUrl: string; 5 | csrfTokenUrl?: string | null; 6 | } 7 | -------------------------------------------------------------------------------- /src/interfaces/AuthContextType.ts: -------------------------------------------------------------------------------- 1 | import type { User } from './User'; 2 | 3 | export interface AuthContextType { 4 | getToken: () => Promise; 5 | currentUser: User | null; 6 | updateUser: () => Promise; 7 | isAuthenticated: boolean; 8 | setUserIsAuthenticated: (userIsAuthenticated: boolean) => void; 9 | login: ( 10 | email: string, 11 | password: string, 12 | deviceName: string 13 | ) => Promise; 14 | logout: () => Promise; 15 | } 16 | -------------------------------------------------------------------------------- /src/interfaces/AuthProviderProps.ts: -------------------------------------------------------------------------------- 1 | import { type AuthConfig } from './AuthConfig'; 2 | import { type ReactNode } from 'react'; 3 | 4 | export interface AuthProviderProps { 5 | config: AuthConfig; 6 | children: ReactNode; 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/AuthResponse.ts: -------------------------------------------------------------------------------- 1 | import type { User } from './User'; 2 | 3 | export interface AuthResponse { 4 | token: string; 5 | user: User; 6 | } 7 | -------------------------------------------------------------------------------- /src/interfaces/User.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | name: string; 4 | email: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/services/AuthService.ts: -------------------------------------------------------------------------------- 1 | import TokenStorage from '../utils/TokenStorage'; 2 | import type { AuthConfig, User } from 'react-native-laravel-sanctum'; 3 | 4 | class AuthService { 5 | private readonly config: AuthConfig | null; 6 | private csrfToken: string | null = null; // CSRF-Token speichern 7 | 8 | constructor(authConfig: AuthConfig) { 9 | if (authConfig === null) { 10 | throw new Error('AuthConfig is null'); 11 | } 12 | this.config = authConfig; 13 | } 14 | 15 | private async handleResponse(response: Response): Promise { 16 | if (!response.ok) { 17 | throw new Error('Request was not successful'); 18 | } 19 | } 20 | 21 | private async fetchCSRFToken() { 22 | try { 23 | if (!this.config || !this.config.csrfTokenUrl) { 24 | return; 25 | } 26 | 27 | const response = await fetch(this.config.csrfTokenUrl, { 28 | method: 'GET', 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | }, 32 | }); 33 | 34 | await this.handleResponse(response); 35 | 36 | // Extrahieren des CSRF-Tokens aus dem Set-Cookie-Header 37 | const setCookieHeader = response.headers.get('set-cookie'); 38 | if (setCookieHeader) { 39 | const csrfTokenMatch = setCookieHeader.match(/XSRF-TOKEN=([^;]*)/); 40 | if (csrfTokenMatch) { 41 | this.csrfToken = csrfTokenMatch[1] ?? null; 42 | } 43 | } 44 | } catch (error) { 45 | console.error('Error while fetching CSRF token:', error); 46 | throw error; 47 | } 48 | } 49 | 50 | private async getRequestHeaders() { 51 | const headers: Record = { 52 | 'Content-Type': 'application/json', 53 | }; 54 | 55 | if (this.csrfToken) { 56 | headers['X-XSRF-TOKEN'] = this.csrfToken; 57 | } 58 | 59 | const currentToken = await TokenStorage.getToken(); 60 | if (currentToken) { 61 | headers.Authorization = `Bearer ${currentToken}`; 62 | } 63 | 64 | return headers; 65 | } 66 | 67 | async login( 68 | email: string, 69 | password: string, 70 | deviceName: string 71 | ): Promise { 72 | try { 73 | if (!this.config) { 74 | throw new Error('Authentication configuration is missing'); 75 | } 76 | 77 | if (this.config.csrfTokenUrl) { 78 | await this.fetchCSRFToken(); 79 | } 80 | 81 | const response = await fetch(this.config.loginUrl, { 82 | method: 'POST', 83 | headers: await this.getRequestHeaders(), 84 | body: JSON.stringify({ 85 | email, 86 | password, 87 | deviceName, 88 | }), 89 | }); 90 | 91 | await this.handleResponse(response); 92 | 93 | const data = await response.json(); 94 | const { token } = data; 95 | 96 | if (token) { 97 | await TokenStorage.saveToken(token); 98 | return true; 99 | } else { 100 | return false; 101 | } 102 | } catch (error) { 103 | console.error('Error during login:', error); 104 | throw error; 105 | } 106 | } 107 | 108 | async logout(): Promise { 109 | try { 110 | if (!this.config) { 111 | throw new Error('Authentication configuration is missing'); 112 | } 113 | 114 | if (this.config.csrfTokenUrl) { 115 | await this.fetchCSRFToken(); 116 | } 117 | 118 | const currentToken = await TokenStorage.getToken(); 119 | 120 | if (currentToken === null) { 121 | return true; 122 | } 123 | 124 | const response = await fetch(this.config.logoutUrl, { 125 | method: 'POST', 126 | headers: await this.getRequestHeaders(), 127 | }); 128 | 129 | await this.handleResponse(response); 130 | await TokenStorage.removeToken(); 131 | return true; 132 | } catch (error) { 133 | console.error('Error during logout:', error); 134 | throw error; 135 | } 136 | } 137 | 138 | async getUser(): Promise { 139 | try { 140 | if (!this.config) { 141 | throw new Error('Authentication configuration is missing'); 142 | } 143 | 144 | const currentToken = await TokenStorage.getToken(); 145 | 146 | if (currentToken === null) { 147 | return null; 148 | } 149 | 150 | if (this.config.csrfTokenUrl) { 151 | await this.fetchCSRFToken(); 152 | } 153 | 154 | const response = await fetch(this.config.userUrl, { 155 | method: 'GET', 156 | headers: await this.getRequestHeaders(), 157 | }); 158 | 159 | await this.handleResponse(response); 160 | 161 | const user = await response.json(); 162 | 163 | if (user) { 164 | this.csrfToken = null; 165 | return user; 166 | } else { 167 | return null; 168 | } 169 | } catch (error) { 170 | console.error('Error while fetching user:', error); 171 | throw error; 172 | } 173 | } 174 | } 175 | 176 | export default AuthService; 177 | -------------------------------------------------------------------------------- /src/utils/TokenStorage.native.ts: -------------------------------------------------------------------------------- 1 | import * as SecureStore from 'expo-secure-store'; 2 | 3 | class TokenStorage { 4 | private static readonly TOKEN_KEY = 'authToken'; 5 | 6 | static async saveToken(token: string): Promise { 7 | try { 8 | await SecureStore.setItemAsync(TokenStorage.TOKEN_KEY, token); 9 | } catch (error) { 10 | console.error('Error while saving the encrypted token:', error); 11 | throw error; 12 | } 13 | } 14 | 15 | static async getToken(): Promise { 16 | try { 17 | const encryptedToken = await SecureStore.getItemAsync( 18 | TokenStorage.TOKEN_KEY 19 | ); 20 | return encryptedToken || null; 21 | } catch (error) { 22 | console.error('Error while retrieving the encrypted token:', error); 23 | throw error; 24 | } 25 | } 26 | 27 | static async removeToken(): Promise { 28 | try { 29 | await SecureStore.deleteItemAsync(TokenStorage.TOKEN_KEY); 30 | } catch (error) { 31 | console.error('Error while removing the encrypted token:', error); 32 | throw error; 33 | } 34 | } 35 | } 36 | 37 | export default TokenStorage; 38 | -------------------------------------------------------------------------------- /src/utils/TokenStorage.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | 3 | class TokenStorage { 4 | private static readonly TOKEN_KEY = 'authToken'; 5 | 6 | static async saveToken(token: string): Promise { 7 | try { 8 | await AsyncStorage.setItem(TokenStorage.TOKEN_KEY, token); 9 | } catch (error) { 10 | console.error('Error while saving the encrypted token:', error); 11 | throw error; 12 | } 13 | } 14 | 15 | static async getToken(): Promise { 16 | try { 17 | const localToken = await AsyncStorage.getItem(TokenStorage.TOKEN_KEY); 18 | return localToken || null; 19 | } catch (error) { 20 | console.error('Error while retrieving the encrypted token:', error); 21 | throw error; 22 | } 23 | } 24 | 25 | static async removeToken(): Promise { 26 | try { 27 | await AsyncStorage.removeItem(TokenStorage.TOKEN_KEY); 28 | } catch (error) { 29 | console.error('Error while removing the encrypted token:', error); 30 | throw error; 31 | } 32 | } 33 | } 34 | 35 | export default TokenStorage; 36 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": ["example"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "react-native-laravel-sanctum": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "lib": ["esnext"], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": true, 17 | "noImplicitUseStrict": false, 18 | "noStrictGenericChecks": false, 19 | "noUncheckedIndexedAccess": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "target": "esnext", 26 | "verbatimModuleSyntax": true 27 | } 28 | } 29 | --------------------------------------------------------------------------------