diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 843045e..24ee2cc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Create a report to help us improve title: '' labels: bug assignees: '' - --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,9 +24,10 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Device (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - React Native and MeteorRN versions: [e.g. 0.62.1, 2.0.0] + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- React Native and MeteorRN versions: [e.g. 0.62.1, 2.0.0] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/companion-package-request.md b/.github/ISSUE_TEMPLATE/companion-package-request.md index d31214f..3c39dee 100644 --- a/.github/ISSUE_TEMPLATE/companion-package-request.md +++ b/.github/ISSUE_TEMPLATE/companion-package-request.md @@ -4,7 +4,6 @@ about: Suggest an idea for a new companion package title: '' labels: '' assignees: '' - --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/ISSUE_TEMPLATE/core-feature-request.md b/.github/ISSUE_TEMPLATE/core-feature-request.md index 2fc593f..f920b23 100644 --- a/.github/ISSUE_TEMPLATE/core-feature-request.md +++ b/.github/ISSUE_TEMPLATE/core-feature-request.md @@ -4,7 +4,6 @@ about: Suggest an idea for this project that affects @meteorrn/core title: '' labels: core-enhancement assignees: '' - --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3a1a770..1635c11 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,11 +1,9 @@ -name: "CodeQL" +name: 'CodeQL' on: push: - branches: [master, ] - pull_request: - # The branches below must be a subset of the branches above branches: [master] + pull_request: # check code ql on every PR! schedule: - cron: '0 22 * * 3' @@ -15,40 +13,40 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/manual.yml b/.github/workflows/manual.yml index 4cfe17a..cd79a12 100644 --- a/.github/workflows/manual.yml +++ b/.github/workflows/manual.yml @@ -25,6 +25,6 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Runs a single command using the runners shell - - name: Send review message - run: echo "This PR for release ${{ github.event.inputs.name }} needs review from the community. Check out the changelog and provide your feedback!" + # Runs a single command using the runners shell + - name: Send review message + run: echo "This PR for release ${{ github.event.inputs.name }} needs review from the community. Check out the changelog and provide your feedback!" diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index f7b20bb..6c4e92f 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -1,27 +1,73 @@ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions -name: Node.js CI +name: Test Suite on: push: - branches: [ master ] - pull_request: - branches: [ master ] + branches: [master] + pull_request: # run on all pull requests jobs: - build: - + lintjs: + name: Javascript standard lint runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: cache dependencies + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - run: npm ci --legacy-peer-deps + - run: npm run lint + + unittest: + name: unit tests + runs-on: ubuntu-latest + needs: [lintjs] strategy: matrix: - node-version: [10.x, 12.x] + node: [14, 16, 18] + steps: + - name: Checkout ${{ matrix.node }} + uses: actions/checkout@v3 + - name: Setup node ${{ matrix.node }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + + - name: Cache dependencies ${{ matrix.node }} + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-${{ matrix.node }} + - run: npm ci --legacy-peer-deps + - run: npm run test:coverage + + build: + name: Build + runs-on: ubuntu-latest + needs: [unittest] + strategy: + matrix: + node: [14, 16, 18] steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm run build --if-present + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - run: npm run build --if-present diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 74262fe..af86958 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -2,18 +2,17 @@ name: Mark stale issues and pull requests on: schedule: - - cron: "30 1 * * *" + - cron: '30 1 * * *' jobs: stale: - runs-on: ubuntu-latest steps: - - uses: actions/stale@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'Closing this issue due to no activity. Feel free to reopen.' - stale-pr-message: 'Closing this PR due to no activity. Feel free to reopen.' - stale-issue-label: 'no-issue-activity' - stale-pr-label: 'no-pr-activity' + - uses: actions/stale@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'Closing this issue due to no activity. Feel free to reopen.' + stale-pr-message: 'Closing this PR due to no activity. Feel free to reopen.' + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' diff --git a/.mocharc.js b/.mocharc.js index 31b73e1..0f75f7a 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -5,10 +5,10 @@ babelRegister(); // for more options see here https://github.com/mochajs/mocha/blob/master/example/config/.mocharc.yml module.exports = { recursive: true, - reporter: "spec", + reporter: 'spec', retries: 0, slow: 20, timeout: 2000, - ui: "bdd", - require: ['test/hooks/mockServer.js'] -} + ui: 'bdd', + require: ['test/hooks/mockServer.js'], +}; diff --git a/.nycrc.json b/.nycrc.json index e1552da..45f9c89 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -1,3 +1,3 @@ { - "extends": "@istanbuljs/nyc-config-babel" -} \ No newline at end of file + "extends": "@istanbuljs/nyc-config-babel" +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index f8bf8d3..d02f58c 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bcbe283..ab29a26 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ Thanks for your interest in contributing! All PRs must address **one** feature or issue, and may only modify **one** package (unless there is an issue/feature that absolutely requires an update across multiple packages). Before you submit a PR, please make sure to test your update on iOS and Android in release mode. For more info on testing, see Testing below ### Contributing to docs + All PRs must address **one** feature or issue, and may only modify **one** package (unless there is an issue/feature that absolutely requires an update across multiple packages). # Testing @@ -22,12 +23,15 @@ All code-level PRs must be tested on a real device, simulators/emulators are not Once you have your testing app installed on a device, use the following test cases depending on what features your update interacts with: **All Updates:** + - Device with Internet Connection - Device that is Offline **Updates that interact with `AsyncStorage`, `NetInfo`, or `trackr`:** + - Opening from background (device put in sleep mode for at least 60 seconds, then reopened) **Updates that interact with `NetInfo`:** + - Device is on WiFi - Device is on Cellular diff --git a/README.md b/README.md index 352a1b1..ff2cb32 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Meteor React Native + A set of packages allowing you to connect your React Native app to your Meteor server, and take advantage of Meteor-specific features like accounts, reactive data trackers, etc. Compatible with the latest version of React Native. +[![Node.js CI](https://github.com/meteorrn/meteor-react-native/actions/workflows/node.js.yml/badge.svg)](https://github.com/meteorrn/meteor-react-native/actions/workflows/node.js.yml) +[![CodeQL](https://github.com/meteorrn/meteor-react-native/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/meteorrn/meteor-react-native/actions/workflows/codeql-analysis.yml) +![npm](https://img.shields.io/npm/dm/@meteorrn/core) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) + Check out [the @meteorrn github org](https://github.com/meteorrn) for more packages, examples, and tutorials. [Full API Documentation](/docs/api.md) @@ -8,9 +14,10 @@ Check out [the @meteorrn github org](https://github.com/meteorrn) for more packa If you're new to React Native, you can view a guide to using React Native with Meteor on the [Official Meteor Guide](https://guide.meteor.com/react-native.html) # Installation + 1. `npm install --save @meteorrn/core` 2. Confirm you have peer dependencty `@react-native-community/netinfo` installed -3. Confirm you have `@react-native-async-storage/async-storage@>=1.8.1` installed. If you are using Expo, or otherwise cannot use `@react-native-async-storage/async-storage`, see *Custom Storage Adapter* below. +3. Confirm you have `@react-native-async-storage/async-storage@>=1.8.1` installed. If you are using Expo, or otherwise cannot use `@react-native-async-storage/async-storage`, see _Custom Storage Adapter_ below.

A note on AsyncStorage

This package uses `@react-native-async-storage/async-storage` by default. This may cause issues if you are using certain React Native versions, or if you are using Expo. To use a custom AsyncStorage implementation, pass it as an option in `Meteor.connect`: @@ -20,10 +27,10 @@ import { AsyncStorage } from 'react-native'; // ... -Meteor.connect("wss://myapp.meteor.com/websocket", { AsyncStorage }); +Meteor.connect('wss://myapp.meteor.com/websocket', { AsyncStorage }); ``` -If you are using the `AsyncStorage` API yourself, its important that you use the same version that MeteorRN is using, or issues could be caused due to the conflicting versions. Make sure you are using the same AsyncStorage you pass into Meteor (or `@react-native-async-storage/async-storage` if you aren't passing anything), or you can use [MeteorRN's package interface](#package-interface). +If you are using the `AsyncStorage` API yourself, its important that you use the same version that MeteorRN is using, or issues could be caused due to the conflicting versions. Make sure you are using the same AsyncStorage you pass into Meteor (or `@react-native-async-storage/async-storage` if you aren't passing anything), or you can use [MeteorRN's package interface](#package-interface). # Basic Usage @@ -31,30 +38,30 @@ If you are using the `AsyncStorage` API yourself, its important that you use the import Meteor, { Mongo, withTracker } from '@meteorrn/core'; // "mycol" should match the name of the collection on your meteor server, or pass null for a local collection -let MyCol = new Mongo.Collection("mycol"); +let MyCol = new Mongo.Collection('mycol'); -Meteor.connect("wss://myapp.meteor.com/websocket"); // Note the /websocket after your URL +Meteor.connect('wss://myapp.meteor.com/websocket'); // Note the /websocket after your URL class App extends React.Component { - render() { - let {myThing} = this.props; - - return ( - - Here is the thing: {myThing.name} - - ); - } + render() { + let { myThing } = this.props; + + return ( + + Here is the thing: {myThing.name} + + ); + } } let AppContainer = withTracker(() => { - Meteor.subscribe("myThing"); - let myThing = MyCol.findOne(); - - return { - myThing - }; -})(App) + Meteor.subscribe('myThing'); + let myThing = MyCol.findOne(); + + return { + myThing, + }; +})(App); export default AppContainer; ``` @@ -67,17 +74,20 @@ Running the app on a physical device but want to connect to local development ma The `@meteorrn/core` package has been kept as light as possible. To access more features, you can use companion packages. Here are some examples: + - `@meteorrn/oauth-google`: Allows you to let users login to your app with Google - `@meteorrn/oauth-facebook`: Allows you to let users login to your app with Facebook For the full list of officially recognized packages, check out [the @meteorrn github org](https://github.com/meteorrn). # Compatibility + This package is compatible with React Native versions from 0.60.0 to latest (0.63.2) For React Native <0.60.0 use [react-native-meteor](https://github.com/inProgress-team/react-native-meteor). **Migrating from `react-native-meteor`:** + - cursoredFind is no longer an option. All .find() calls will return cursors (to match Meteor) - `MeteorListView` & `MeteorComplexListView` have been removed - `CollectionFS` has been removed @@ -86,44 +96,48 @@ For React Native <0.60.0 use [react-native-meteor](https://github.com/inProgress - `composeWithTracker` has been removed # Using on Web + While this package was designed with React Native in mind, it is also capable of running on web (using `react-dom`). This can be useful if you need a light-weight Meteor implementation, if you want to create a client app separate from your server codebase, etc. The only change required is providing an AsyncStorage implementation. Here is a simple example: -```` +``` const AsyncStorage = { setItem:async (key, value) => window.localStorage.setItem(key, value), getItem:async (key) => window.localStorage.getItem(key) - removeItem:async (key) => window.localStorage.removeItem(key) + removeItem:async (key) => window.localStorage.removeItem(key) } Meteor.connect("wss://.../websock", {AsyncStorage}); -```` +``` # Changelog + The [GitHub Releases Tab](https://github.com/TheRealNate/meteor-react-native/releases) includes a full changelog # Package Interface To ensure that MeteorRN companion packages use the same versions of external packages like AsyncStorage as the core, `@meteorrn/core` provides a package interface, where companion packages can access certain packages. Currently package interface returns an object with the following properties: -- AsyncStorage + +- AsyncStorage ### Usage -```` + +``` import Meteor from '@meteorrn/core'; const {AsyncStorage} = Meteor.packageInterface(); -```` - +``` ### Differences from Meteor Core to Note: + - This API does not implement `observeChanges` (but it does implement `observe`) # Showcase -| Whazzup.co | StarlingRealtime | -| --- | --- | +| Whazzup.co | StarlingRealtime | +| --------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | | | -| [Whazzup.co](https://whazzup.co/) uses Meteor React Native in their native app | [StarlingRealtime](https://www.starlingrealtime.com/) uses Meteor React Native in their production app | +| [Whazzup.co](https://whazzup.co/) uses Meteor React Native in their native app | [StarlingRealtime](https://www.starlingrealtime.com/) uses Meteor React Native in their production app |
diff --git a/SECURITY.md b/SECURITY.md index 3104e5e..307f21c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Version | Supported | | ------- | ------------------ | -| >=2.0.0 | :white_check_mark: | +| >=2.0.0 | :white_check_mark: | ## Reporting a Vulnerability -To contact the lead developer for this package (@TheRealNate) directly about a security issue, please email nate@kidzideaz.tech. +To contact the lead developer for this package (@TheRealNate) directly about a security issue, please email nate@kidzideaz.tech. diff --git a/companion-packages/meteorrn-local/README.md b/companion-packages/meteorrn-local/README.md index b5842f8..da6863c 100644 --- a/companion-packages/meteorrn-local/README.md +++ b/companion-packages/meteorrn-local/README.md @@ -1,34 +1,36 @@ # Local -This package allows you to store your data locally (similar to GroundDB for Meteor Web). +This package allows you to store your data locally (similar to GroundDB for Meteor Web). This package introduces the `Local.Collection`, which will mirror the specified remote collection, and store all documents on the device, making your data offline. ### Caveats + - This package (currently) works by creating a second local Mongo Collection. This means you are esentially keeping two copies of each document (that you store locally) in memory. This issue can be mitigated by keeping an "age" or "version" on all your documents, and only publishing documents that have been changed from local - This package (currently) does not support the automatic removal/expiry of documents. Once a document has been inserted into the local database, it is there forever (unless you manually call `remove` on the Local.Collection) ### Usage: -```` +``` import Local from '@meteorrn/local'; const MyLocalCollection = new Local.Collection("name"); MyLocalCollection.find().fetch() -```` +``` You should use LocalCollection whenever you want to access the stored data. The Local Collection will observe the live collection and automatically update when the live collection does. ### Data Loading: + A `Local.Collection` exposes a property called `loadPromise` which resolves once local data has been loaded into the collection. You can use this to control loading flow, like so: -```` +``` const Todos = new Local.Collection("todos"); class Home extends React.Component { state = {dataLoading:true}; - + componentDidMount() { Todos.loadPromise.then(() => { this.setState({dataLoading:false}); @@ -37,30 +39,33 @@ class Home extends React.Component { }); } } -```` +``` ### Data Grouping + By default, this package stores each collection in its own AsyncStorage field. If you plan to store very large amounts of data in a collection, consider grouping the data. When you specify a certain field, data will be grouped on this field and stored in separate AsyncStorage fields. If you specify a `limit`, the limit will be applied to individual groups instead of the collection as a whole. ### API Docs #### Collection(name, options) -Creates a Local Collection that mirrors changes to collection with specified name. + +Creates a Local Collection that mirrors changes to collection with specified name. **Options:** -*groupBy (default: null):* Specifies a field to organize items on. Items will be grouped into separate AsyncStorage keys by specified limit. If you specifiy a limit, the limit will be applied to each group instead of the collection as a whole +_groupBy (default: null):_ Specifies a field to organize items on. Items will be grouped into separate AsyncStorage keys by specified limit. If you specifiy a limit, the limit will be applied to each group instead of the collection as a whole -*limit (default: -1):* Specifies a limit to the number of documents to store. The sort property is required to use this. +_limit (default: -1):_ Specifies a limit to the number of documents to store. The sort property is required to use this. -*sort (default: null):* Specifies a sort method to maintain documents by +_sort (default: null):_ Specifies a sort method to maintain documents by -*disableDateParser (default: false):* Disables the default behavior when parsing the stringified collection of automatically converting date strings into JS dates +_disableDateParser (default: false):_ Disables the default behavior when parsing the stringified collection of automatically converting date strings into JS dates **Properties:** A `Local.Collection` is a local Mongo Collection that exposes the following additional properties -*loadPromise (Promise):* A promise that resolves when the local data has been inserted into the collection. While this will typically only take a few hundred milliseconds, if you have UI that depends on the local data, you may want to use this promise in your loading flow. +_loadPromise (Promise):_ A promise that resolves when the local data has been inserted into the collection. While this will typically only take a few hundred milliseconds, if you have UI that depends on the local data, you may want to use this promise in your loading flow. ### Compatability + This package takes advantage of observe and local collections, added in `@meteorrn/core@2.0.8`. diff --git a/companion-packages/meteorrn-local/index.js b/companion-packages/meteorrn-local/index.js index b6c6c0a..fb32b35 100644 --- a/companion-packages/meteorrn-local/index.js +++ b/companion-packages/meteorrn-local/index.js @@ -1,121 +1,133 @@ import { Mongo, packageInterface } from '@meteorrn/core'; -const stringifiedDateRegExp = new RegExp(/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}Z/); -let fixDates = function (k,v) { - if(stringifiedDateRegExp.test(v)) { - return new Date(v); - } - return v; +const stringifiedDateRegExp = new RegExp( + /[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}Z/ +); +let fixDates = function (k, v) { + if (stringifiedDateRegExp.test(v)) { + return new Date(v); + } + return v; }; -const defaultOptions = {disableDateParser:false, batchUpdates:false}; +const defaultOptions = { disableDateParser: false, batchUpdates: false }; const Local = { - Collection:function (name, _options={}) { - const { AsyncStorage } = packageInterface(); - const options = Object.assign({}, defaultOptions, _options); - - const LiveCol = new Mongo.Collection(name); - const LocalCol = new Mongo.Collection(null); - let batchQueued = false; - - const _storeLocalCol = async () => { - batchQueued = false; - const data = LocalCol.find({}, {sort:options.sort}).fetch(); - if(options.groupBy) { - const groups = {}; - - for(let d of data) { - const v = d[options.groupBy]; - groups[v] = groups[v] || []; - groups[v].push(d); - } - - for(let g in groups) { - await AsyncStorage.setItem("@mrnlocal:" + name + ":" + g, JSON.stringify(groups[g].slice(0, options.limit))); - } - - await AsyncStorage.setItem("@mrnlocal:" + name + "_groups", JSON.stringify(Object.keys(groups))); - } - else { - await AsyncStorage.setItem("@mrnlocal:" + name, JSON.stringify(data)); - } - }; - - const storeLocalCol = async () => { - if(options.batchUpdates) { - if(batchQueued) { - return; - } - else { - batchQueued = true; - setTimeout(_storeLocalCol, 150); - return; - } - } - else { - await _storeLocalCol(); - } - }; - - const loadData = async () => { - - if(options.groupBy) { - let groups = JSON.parse((await AsyncStorage.getItem("@mrnlocal:" + name + "_groups")) || "[]"); - for(let g of groups) { - const storedData = await AsyncStorage.getItem("@mrnlocal:" + name + ":" + g); - if(storedData) { - const documents = JSON.parse(storedData, options.disableDateParser ? v=>v : fixDates); - documents.forEach(doc => { - LocalCol._collection.upsert(doc); - }); - } - } - } - else { - const storedData = await AsyncStorage.getItem("@mrnlocal:" + name); - if(storedData) { - const documents = JSON.parse(storedData, options.disableDateParser ? v=>v : fixDates); - documents.forEach(doc => { - LocalCol._collection.upsert(doc); - }); - } - } - - LiveCol.find({}).observe({ - added:async doc => { - LocalCol._collection.upsert(doc); - storeLocalCol(); - }, - changed:async doc => { - LocalCol._collection.upsert(doc); - storeLocalCol(); - } + Collection: function (name, _options = {}) { + const { AsyncStorage } = packageInterface(); + const options = Object.assign({}, defaultOptions, _options); + + const LiveCol = new Mongo.Collection(name); + const LocalCol = new Mongo.Collection(null); + let batchQueued = false; + + const _storeLocalCol = async () => { + batchQueued = false; + const data = LocalCol.find({}, { sort: options.sort }).fetch(); + if (options.groupBy) { + const groups = {}; + + for (let d of data) { + const v = d[options.groupBy]; + groups[v] = groups[v] || []; + groups[v].push(d); + } + + for (let g in groups) { + await AsyncStorage.setItem( + '@mrnlocal:' + name + ':' + g, + JSON.stringify(groups[g].slice(0, options.limit)) + ); + } + + await AsyncStorage.setItem( + '@mrnlocal:' + name + '_groups', + JSON.stringify(Object.keys(groups)) + ); + } else { + await AsyncStorage.setItem('@mrnlocal:' + name, JSON.stringify(data)); + } + }; + + const storeLocalCol = async () => { + if (options.batchUpdates) { + if (batchQueued) { + return; + } else { + batchQueued = true; + setTimeout(_storeLocalCol, 150); + return; + } + } else { + await _storeLocalCol(); + } + }; + + const loadData = async () => { + if (options.groupBy) { + let groups = JSON.parse( + (await AsyncStorage.getItem('@mrnlocal:' + name + '_groups')) || '[]' + ); + for (let g of groups) { + const storedData = await AsyncStorage.getItem( + '@mrnlocal:' + name + ':' + g + ); + if (storedData) { + const documents = JSON.parse( + storedData, + options.disableDateParser ? (v) => v : fixDates + ); + documents.forEach((doc) => { + LocalCol._collection.upsert(doc); }); - }; - - - LocalCol.__insert = LocalCol.insert; - LocalCol.__update = LocalCol.update; - LocalCol.__remove = LocalCol.remove; - - LocalCol.insert = (...args) => { - LocalCol.__insert(...args); - storeLocalCol(); - }; - LocalCol.update = (...args) => { - LocalCol.__update(...args); - storeLocalCol(); - }; - LocalCol.remove = (...args) => { - LocalCol.__remove(...args); - storeLocalCol(); - }; - - LocalCol.loadPromise = loadData(); - LocalCol.save = storeLocalCol; - - return LocalCol; - } + } + } + } else { + const storedData = await AsyncStorage.getItem('@mrnlocal:' + name); + if (storedData) { + const documents = JSON.parse( + storedData, + options.disableDateParser ? (v) => v : fixDates + ); + documents.forEach((doc) => { + LocalCol._collection.upsert(doc); + }); + } + } + + LiveCol.find({}).observe({ + added: async (doc) => { + LocalCol._collection.upsert(doc); + storeLocalCol(); + }, + changed: async (doc) => { + LocalCol._collection.upsert(doc); + storeLocalCol(); + }, + }); + }; + + LocalCol.__insert = LocalCol.insert; + LocalCol.__update = LocalCol.update; + LocalCol.__remove = LocalCol.remove; + + LocalCol.insert = (...args) => { + LocalCol.__insert(...args); + storeLocalCol(); + }; + LocalCol.update = (...args) => { + LocalCol.__update(...args); + storeLocalCol(); + }; + LocalCol.remove = (...args) => { + LocalCol.__remove(...args); + storeLocalCol(); + }; + + LocalCol.loadPromise = loadData(); + LocalCol.save = storeLocalCol; + + return LocalCol; + }, }; export default Local; diff --git a/companion-packages/meteorrn-ndev-mfa/README.md b/companion-packages/meteorrn-ndev-mfa/README.md index c22961d..ede0ad2 100644 --- a/companion-packages/meteorrn-ndev-mfa/README.md +++ b/companion-packages/meteorrn-ndev-mfa/README.md @@ -3,6 +3,7 @@ # ndev:mfa for MeteorRN This pacakge allows your MeteorRN app to interact with `ndev:mfa`. It does not support using a security key, but for users with u2f MFA, it takes advantage of the U2F Authorization Code feature. This package exposes the following client methods for MFA. + - useU2FAuthorizationCode - finishLogin - loginWithMFA @@ -10,7 +11,7 @@ This pacakge allows your MeteorRN app to interact with `ndev:mfa`. It does not s Here's a simple login flow: -```` +``` import MFA from '@meteorrn/ndev-mfa'; MFA.login(username, password).then(r => { @@ -19,11 +20,11 @@ MFA.login(username, password).then(r => { } else { let code = await collectTheCodeSomehow(); - + if(r.method === "u2f") { code = MFA.useU2FAuthorizationCode(code); } - + MFA.finishLogin(r.finishLoginParams, code).then(() => { // Login Complete }).catch(err => { @@ -34,4 +35,4 @@ MFA.login(username, password).then(r => { // Error (Incorrect Password? Invalid Account?) }); -```` +``` diff --git a/companion-packages/meteorrn-ndev-mfa/index.js b/companion-packages/meteorrn-ndev-mfa/index.js index bebbac3..ef83134 100644 --- a/companion-packages/meteorrn-ndev-mfa/index.js +++ b/companion-packages/meteorrn-ndev-mfa/index.js @@ -3,96 +3,110 @@ import Meteor, { Accounts } from '@meteorrn/core'; import { loginChallengeHandler, loginCompletionHandler } from './method-names'; let useU2FAuthorizationCode = function (code) { - if(typeof(code) !== "string" || code.length !== 6) { - throw new Error("Invalid Code"); - } - - return {U2FAuthorizationCode:code}; + if (typeof code !== 'string' || code.length !== 6) { + throw new Error('Invalid Code'); + } + + return { U2FAuthorizationCode: code }; }; -let assembleChallengeCompletionArguments = async function (finishLoginParams, code) { - let {res} = finishLoginParams; - let methodArguments = []; - - if(res.method === "u2f") { - let assertion; - if(code && code.U2FAuthorizationCode) { - /* +let assembleChallengeCompletionArguments = async function ( + finishLoginParams, + code +) { + let { res } = finishLoginParams; + let methodArguments = []; + + if (res.method === 'u2f') { + let assertion; + if (code && code.U2FAuthorizationCode) { + /* We require that the MFA.useU2FAuthorizationCode method is used even though we just pull the code out to make sure the code isn't actually an OTP due to a coding error. */ - let {challengeId, challengeSecret} = finishLoginParams.res; - assertion = {challengeId, challengeSecret, ...code}; - } - else { - throw new Error("Code must be a U2FAuthorizationCode"); - } - methodArguments.push(assertion); + let { challengeId, challengeSecret } = finishLoginParams.res; + assertion = { challengeId, challengeSecret, ...code }; + } else { + throw new Error('Code must be a U2FAuthorizationCode'); } + methodArguments.push(assertion); + } - if(res.method === "otp" || res.method === "totp") { - if(!code) { - throw new Meteor.Error("otp-required", "An OTP is required"); - } - - methodArguments.push({...res, code}); - } - - return methodArguments; + if (res.method === 'otp' || res.method === 'totp') { + if (!code) { + throw new Meteor.Error('otp-required', 'An OTP is required'); + } + + methodArguments.push({ ...res, code }); + } + + return methodArguments; }; -let finishLogin = (finishLoginParams, code) => new Promise(async (resolve, reject) => { +let finishLogin = (finishLoginParams, code) => + new Promise(async (resolve, reject) => { let methodName = loginCompletionHandler(); - let methodArguments = await assembleChallengeCompletionArguments(finishLoginParams, code); - + let methodArguments = await assembleChallengeCompletionArguments( + finishLoginParams, + code + ); + Meteor._startLoggingIn(); Meteor.call(methodName, ...methodArguments, (err, result) => { - Meteor._endLoggingIn(); - Meteor._handleLoginCallback(err, result); - - if(err) { - reject(err); - } - else { - resolve(); - } - }, - ); -}); - -let loginWithMFA = (username, password) => new Promise((resolve, reject) => { - Meteor.call(loginChallengeHandler(), username, Accounts._hashPassword(password), async (err, res) => { - if(err) { - return reject(err); - } - - let finishLoginParams = {res, _type:"login"}; - let doesSupportU2FLogin = false; - - resolve({supportsU2FLogin:doesSupportU2FLogin, method:res.method, finishLoginParams, finishParams:finishLoginParams}); + Meteor._endLoggingIn(); + Meteor._handleLoginCallback(err, result); + + if (err) { + reject(err); + } else { + resolve(); + } }); -}); - -let login = (username, password) => new Promise((resolve, reject) => { - Meteor.loginWithPassword(username, password, err => { - if(err) { - if(err.error === "mfa-required") { - loginWithMFA(username, password).then(resolve).catch(reject); - } - else { - reject(err); - } + }); + +let loginWithMFA = (username, password) => + new Promise((resolve, reject) => { + Meteor.call( + loginChallengeHandler(), + username, + Accounts._hashPassword(password), + async (err, res) => { + if (err) { + return reject(err); } - else { - resolve({method:null}); + + let finishLoginParams = { res, _type: 'login' }; + let doesSupportU2FLogin = false; + + resolve({ + supportsU2FLogin: doesSupportU2FLogin, + method: res.method, + finishLoginParams, + finishParams: finishLoginParams, + }); + } + ); + }); + +let login = (username, password) => + new Promise((resolve, reject) => { + Meteor.loginWithPassword(username, password, (err) => { + if (err) { + if (err.error === 'mfa-required') { + loginWithMFA(username, password).then(resolve).catch(reject); + } else { + reject(err); } + } else { + resolve({ method: null }); + } }); -}); + }); export default { - useU2FAuthorizationCode, - finishLogin, - loginWithMFA, - login, + useU2FAuthorizationCode, + finishLogin, + loginWithMFA, + login, }; diff --git a/companion-packages/meteorrn-ndev-mfa/method-names.js b/companion-packages/meteorrn-ndev-mfa/method-names.js index 23678b4..6daccda 100644 --- a/companion-packages/meteorrn-ndev-mfa/method-names.js +++ b/companion-packages/meteorrn-ndev-mfa/method-names.js @@ -1,13 +1,20 @@ -export let registrationChallengeHandlerU2F = () => "registrationChallengeHandlerU2F"; -export let registerCompletionHandlerU2F = () => "registerCompletionHandlerU2F"; -export let loginChallengeHandler = () => "loginChallengeHandler"; -export let loginCompletionHandlerU2F = () => "loginCompletionHandlerU2F"; -export let loginCompletionHandlerOTP = () => "loginCompletionHandlerOTP"; -export let resetPasswordChallengeHandler = () => "resetPasswordChallengeHandler"; -export let checkResetPasswordHasMFA = () => "checkResetPasswordHasMFA"; -export let registrationChallengeHandlerTOTP = () => "registrationChallengeHandlerTOTP"; -export let registrationCompletionHandlerTOTP = () => "registrationCompletionHandlerTOTP"; -export let loginCompletionHandler = () => "loginCompletionHandler"; -export let resetPasswordCheckMFARequired = () => "resetPasswordCheckMFARequired"; -export let authorizeActionChallengeHandler = () => "authorizeActionChallengeHandler"; -export let authorizeActionCompletionHandler = () => "authorizeActionCompletionHandler"; \ No newline at end of file +export let registrationChallengeHandlerU2F = () => + 'registrationChallengeHandlerU2F'; +export let registerCompletionHandlerU2F = () => 'registerCompletionHandlerU2F'; +export let loginChallengeHandler = () => 'loginChallengeHandler'; +export let loginCompletionHandlerU2F = () => 'loginCompletionHandlerU2F'; +export let loginCompletionHandlerOTP = () => 'loginCompletionHandlerOTP'; +export let resetPasswordChallengeHandler = () => + 'resetPasswordChallengeHandler'; +export let checkResetPasswordHasMFA = () => 'checkResetPasswordHasMFA'; +export let registrationChallengeHandlerTOTP = () => + 'registrationChallengeHandlerTOTP'; +export let registrationCompletionHandlerTOTP = () => + 'registrationCompletionHandlerTOTP'; +export let loginCompletionHandler = () => 'loginCompletionHandler'; +export let resetPasswordCheckMFARequired = () => + 'resetPasswordCheckMFARequired'; +export let authorizeActionChallengeHandler = () => + 'authorizeActionChallengeHandler'; +export let authorizeActionCompletionHandler = () => + 'authorizeActionCompletionHandler'; diff --git a/docs/api.md b/docs/api.md index f08c1f1..71f4f02 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,6 +1,7 @@ # Meteor React Native Docs Table of Contents + - [Meteor](#meteor) - [Tracker](#tracker) - [Mongo](#mongo) @@ -10,37 +11,45 @@ Table of Contents `import Meteor from '@meteorrn/core';` - ### `Meteor.connect(url, options)` + Connect to the Meteor Server **url**: The URL of your Meteor Server websocket. This should typically start with `ws://` (insecure, like `http://`) or `wss://` (secure, like `https://`), and have the path `/websocket`, e.g.: `wss://myapp.meteor.com/websocket` **options**: -* autoConnect **boolean** [true] whether to establish the connection to the server upon instantiation. When false, one can manually establish the connection with the Meteor.ddp.connect method. -* autoReconnect **boolean** [true] whether to try to reconnect to the server when the socket connection closes, unless the closing was initiated by a call to the disconnect method. -* reconnectInterval **number** [10000] the interval in ms between reconnection attempts. -* AsyncStorage **object** your preferred AsyncStorage. Defaults to `'@react-native-async-storage/async-storage'` as a peer dependency. + +- autoConnect **boolean** [true] whether to establish the connection to the server upon instantiation. When false, one can manually establish the connection with the Meteor.ddp.connect method. +- autoReconnect **boolean** [true] whether to try to reconnect to the server when the socket connection closes, unless the closing was initiated by a call to the disconnect method. +- reconnectInterval **number** [10000] the interval in ms between reconnection attempts. +- AsyncStorage **object** your preferred AsyncStorage. Defaults to `'@react-native-async-storage/async-storage'` as a peer dependency. ### `Meteor.disconnect()` + Disconnect from the Meteor server ### `Meteor.call(name, [arg1, arg2...], [asyncCallback])` + Perform a call to a method ### `Meteor.subscribe(name, [arg1, arg2, arg3])` + Subscribe to a collection ### `Meteor.user()` + Returns the logged in user ### `Meteor.users` + Access the meteor users collection ### `Meteor.userId()` + Returns the userId of the logged in user ### `Meteor.status()` + Gets the current connection status. Returns an object with the following properties: **connected**: Boolean @@ -48,9 +57,11 @@ Gets the current connection status. Returns an object with the following propert **status**: "connected" || "disconnected" ### `Meteor.loggingIn()` + Returns true if attempting to login ### `Meteor.loggingOut()` + Returns true if attempting to logout ### `Meteor.loginWithPassword` @@ -59,127 +70,128 @@ Returns true if attempting to logout ### `Meteor.logoutOtherClients` - -

Tracker

`import { withTracker, useTracker } from '@meteorrn/core'`; - #### `withTracker(trackerFunc)(Component)` + Creates a new Tracker **Arguments:** - * trackerFunc - Function which will be re-run reactively when it's dependencies are updated. Must return an object that is passed as properties to `Component` - * Component - React Component which will receive properties from trackerFunc +- trackerFunc - Function which will be re-run reactively when it's dependencies are updated. Must return an object that is passed as properties to `Component` +- Component - React Component which will receive properties from trackerFunc #### `useTracker(trackerFunc)` => `React Hook` + Creates a new Tracker React Hook. Can only be used inside a function component. See React Docs for more info. **Arguments:** - * trackerFunc - Function which will be re-run reactively when it's dependencies are updated. - +- trackerFunc - Function which will be re-run reactively when it's dependencies are updated. ## ReactiveDict `import { ReactiveDict } from '@meteorrn/core'` -#### `new ReactiveDict()` => *`ReactiveDict`* -Creates a new reactive dictionary - +#### `new ReactiveDict()` => _`ReactiveDict`_ -#### *`ReactiveDict`* +Creates a new reactive dictionary -***ReactiveDict* Methods:** - * .get(key) - Gets value of key (Reactive) - * .set(key, value) - Sets value of key +#### _`ReactiveDict`_ +**_ReactiveDict_ Methods:** +- .get(key) - Gets value of key (Reactive) +- .set(key, value) - Sets value of key

Mongo

`import { Mongo } from '@meteorrn/core';` #### `new Mongo.Collection(collectionName, options)` => `Collection` -Creates and returns a *Collection* -**Arguments** - * collectionName - Name of the remote collection, or pass `null` for a client-side collection +Creates and returns a _Collection_ +**Arguments** -#### *`Collection`* +- collectionName - Name of the remote collection, or pass `null` for a client-side collection -***Collection* Methods:** - * .insert(document) - Inserts document into collection - * .update(query, modifications) - Updates document in collection - * .remove(query) - Removes document from collection - * .find(query) => *`Cursor`* - Returns a Cursor - * .findOne(query) => Document - Retrieves first matching Document +#### _`Collection`_ +**_Collection_ Methods:** -#### *`Cursor`* +- .insert(document) - Inserts document into collection +- .update(query, modifications) - Updates document in collection +- .remove(query) - Removes document from collection +- .find(query) => _`Cursor`_ - Returns a Cursor +- .findOne(query) => Document - Retrieves first matching Document -***Cursor* Methods:** - * .obsrve() - Mirrors Meteor's observe behavior. Accepts object with the properties `added`, `changed`, and `removed`. - * .fetch() => `[Document]` - Retrieves an array of matching documents +#### _`Cursor`_ +**_Cursor_ Methods:** +- .obsrve() - Mirrors Meteor's observe behavior. Accepts object with the properties `added`, `changed`, and `removed`. +- .fetch() => `[Document]` - Retrieves an array of matching documents

Accounts

`import { Accounts } from '@meteorrn/core';` - #### `Accounts.createUser(user, callback)` + Creates a user **Arguments** - * user - The user object - * callback - Called with a single error object or null on success +- user - The user object +- callback - Called with a single error object or null on success #### `Accounts.changePassword(oldPassword, newPassword)` + Changes a user's password **Arguments** - * oldPassword - The user's current password - * newPassword - The user's new password +- oldPassword - The user's current password +- newPassword - The user's new password #### `Accounts.onLogin(callback)` + Registers a callback to be called when user is logged in **Arguments** - * callback +- callback #### `Accounts.onLoginFailure(callback)` + Registers a callback to be called when login fails **Arguments** - * callback +- callback #### `Accounts._hashPassword(plaintext)` => `{algorithm:"sha-256", digest:"..."}` + Hashes a password using the sha-256 algorithm. Returns an object formatted for use in accounts calls. You can access the raw hashed string using the digest property. **Arguments** - * plaintext - The plaintext string you want to hash - -Other: -* [Accounts.forgotPassword](http://docs.meteor.com/#/full/accounts_changepassword) -* [Accounts.resetPassword](http://docs.meteor.com/#/full/accounts_resetpassword) +- plaintext - The plaintext string you want to hash +Other: +- [Accounts.forgotPassword](http://docs.meteor.com/#/full/accounts_changepassword) +- [Accounts.resetPassword](http://docs.meteor.com/#/full/accounts_resetpassword) ## Verbosity + `import { enableVerbose } from '@meteorrn/core';` Verbose Mode logs detailed information from various places around MeteorRN. **Note:** this will expose login tokens and other private information to the console. - #### `enableVerbose()` + Enables verbose mode diff --git a/docs/installation.md b/docs/installation.md index 256572f..c8309af 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,4 +6,4 @@ This package does not utilize any native modules, so to install, just: Then in your code, make sure you run [`Meteor.connect`](/docs/api.md) to connect to your server. -**Note**: this module contains *breaking changes*, so it will not work for React Native versions under 0.60.0. +**Note**: this module contains _breaking changes_, so it will not work for React Native versions under 0.60.0. diff --git a/examples/Login.jsx b/examples/Login.jsx index 984c8d7..c2d00e2 100644 --- a/examples/Login.jsx +++ b/examples/Login.jsx @@ -3,38 +3,42 @@ import { View, TextInput, Button, Alert } from 'react-native'; import Meteor, { withTracker } from '@meteorrn/core'; class Login extends React.Component { - - state = {email:"", password:""}; + state = { email: '', password: '' }; onLogin = () => { let { email, password } = this.state; - - Meteor.loginWithPassword(email, password, err => { - if(err) { - Alert.alert("Error", err.reason); - } - else { + + Meteor.loginWithPassword(email, password, (err) => { + if (err) { + Alert.alert('Error', err.reason); + } else { // ... } }); - } + }; render() { let { loggingIn } = this.props; - + return ( - Login - - this.setState({email})}/> - - this.setState({password})}/> - - {loggingIn ? - Loading... - : -