diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..168d4a14 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +example/RNBackgroundExample/node_modules +example/RNBackgroundExample/e2e diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..40c6dcd0 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: '@react-native-community', +}; diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..0d0ca920 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,37 @@ + + +# Summary + + + +## Test Plan + + + +### What's required for testing (prerequisites)? + +### What are the steps to reproduce (after prerequisites)? + +## Compatibility + +| OS | Implemented | +| ------- | :---------: | +| iOS | ✅❌ | +| Android | ✅❌ | + +## Checklist + + + +- [ ] I have tested this on a device and a simulator +- [ ] I added the documentation in `README.md` +- [ ] I updated the typed files (TS and Flow) +- [ ] I've added Detox End-to-End Test(s) +- [ ] I've created a snack to demonstrate the changes: LINK HERE diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 00000000..0b74ad72 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,62 @@ +name: Test Android Example App + +on: [push, pull_request] + +jobs: + react-native-android: + runs-on: macos-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + + steps: + - name: Checkout project + uses: actions/checkout@v1 + + - name: Specify node version + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Use specific Java version for sdkmanager to work + uses: joschi/setup-jdk@v1 + with: + java-version: 'openjdk8' + architecture: 'x64' + + - name: Setup Android emulator + run: | + echo y | sudo $ANDROID_HOME/tools/bin/sdkmanager "system-images;android-28;google_apis;x86" > /dev/null + $ANDROID_HOME/tools/bin/avdmanager -s create avd -n emu -k "system-images;android-28;google_apis;x86" -b "x86" -c 1G -d 7 -f + + - name: Install node_modules + working-directory: example/RNBackgroundExample/ + run: + yarn install --frozen-lockfile + + - name: Deploy + working-directory: example/RNBackgroundExample/ + run: + yarn e2e/deploy/android + + - name: Start Emulator + working-directory: example/RNBackgroundExample/ + timeout-minutes: 5 + run: | + export PATH=$PATH:$ANDROID_HOME/platform-tools + $ANDROID_HOME/emulator/emulator @emu -noaudio -no-boot-anim -netdelay none -accel on -no-snapshot & + adb wait-for-device; adb shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;'; adb shell wm dismiss-keyguard + + - name: Android test + working-directory: example/RNBackgroundExample/ + timeout-minutes: 8 + run: | + mkdir -p ./artifacts + node e2e/start-server.js & + adb reverse tcp:8080 tcp:8080 + yarn e2e/test/android --record-videos failing + + - uses: actions/upload-artifact@master + name: Provide videos of failed E2E tests + if: failure() + with: + name: android-failing-e2e-videos + path: example/RNBackgroundExample/artifacts diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml new file mode 100644 index 00000000..755d439e --- /dev/null +++ b/.github/workflows/deploy-release.yml @@ -0,0 +1,28 @@ +name: Deploy & Release + +on: + push: + branches: + - master + +jobs: + deploy-release: + runs-on: macos-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + + steps: + - name: Checkout project + uses: actions/checkout@v1 + + - name: Specify node version + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Release to NPM + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + yarn install --frozen-lockfile + npx semantic-release diff --git a/.github/workflows/iOS.yml b/.github/workflows/iOS.yml new file mode 100644 index 00000000..a7f2c41e --- /dev/null +++ b/.github/workflows/iOS.yml @@ -0,0 +1,40 @@ +name: Test iOS Example App + +on: [push, pull_request] + +jobs: + react-native-ios: + runs-on: macos-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + + steps: + - name: Checkout project + uses: actions/checkout@v1 + + - name: Specify node version + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Setup Detox + run: | + brew tap wix/brew + brew install applesimutils + + - name: Install node_modules + working-directory: example/RNBackgroundExample/ + run: + yarn install --frozen-lockfile + + - name: Deploy + working-directory: example/RNBackgroundExample/ + run: + yarn deploy/release/ios + + - name: iOS test + working-directory: example/RNBackgroundExample/ + timeout-minutes: 8 + run: | + npx detox clean-framework-cache && npx detox build-framework-cache + node e2e/start-server.js & + yarn e2e/test/ios diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml new file mode 100644 index 00000000..a9bb2b37 --- /dev/null +++ b/.github/workflows/node.yml @@ -0,0 +1,24 @@ +name: Node Environment + +on: [push, pull_request] + +jobs: + node-lint-tests: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + + steps: + - name: checkout + uses: actions/checkout@v1 + + - name: setup node + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: install node_modules + run: yarn install --frozen-lockfile + + - name: node lint + run: + yarn lint:ci diff --git a/.gitignore b/.gitignore index e74960bf..0bd27078 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ DerivedData *.ipa *.xcuserstate project.xcworkspace +Pods/ # Android/IntelliJ # diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..355e7d1e --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + bracketSpacing: true, + jsxBracketSameLine: true, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..f6a050d4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +Since Version > 5.3.0 we follow semantic versioning. + +See the [releases](https://github.com/Vydia/react-native-background-upload/releases) page on GitHub for information regarding each release. diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE diff --git a/README.md b/README.md index b7b311ba..8e645a81 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ # react-native-background-upload [![npm version](https://badge.fury.io/js/react-native-background-upload.svg)](https://badge.fury.io/js/react-native-background-upload) + + +![GitHub Actions status](https://github.com/Vydia/react-native-background-upload/workflows/iOS/badge.svg) + +![GitHub Actions status](https://github.com/Vydia/react-native-background-upload/workflows/android/badge.svg) + The only React Native http post file uploader with android and iOS background support. If you are uploading large files like videos, use this so your users can background your app during a long upload. NOTE: Use major version 4 with RN 47.0 and greater. If you have RN less than 47, use 3.0. To view all available versions: @@ -79,7 +85,7 @@ No further actions required. ## 3. Expo -To use this library with [Expo](https://expo.io) one must first detach (eject) the project and follow [step 2](#2-link-native-code) instructions. Additionally on iOS there is a must to add a Header Search Path to other dependencies which are managed using Pods. To do so one has to add `$(SRCROOT)/../../../ios/Pods/Headers/Public` to Header Search Path in `VydiaRNFileUploader` module using XCode. +To use this library with [Expo](https://expo.io) one must first detach (eject) the project and follow [step 2](#2-link-native-code) instructions. Additionally on iOS there is a must to add a Header Search Path to other dependencies which are managed using Pods. To do so one has to add `$(SRCROOT)/../../../ios/Pods/Headers/Public` to Header Search Path in `VydiaRNFileUploader` module using XCode. # Usage @@ -146,7 +152,7 @@ All top-level methods are available as named exports or methods on the default e ### startUpload(options) -The primary method you will use, this starts the upload process. +The primary method you will use, this starts the upload process. Returns a promise with the string ID of the upload. Will reject if there is a connection problem, the file doesn't exist, or there is some other problem. @@ -292,7 +298,7 @@ Is there an example/sandbox app to test out this package? > Yes, there is a simple react native app that comes with an [express](https://github.com/expressjs/express) server where you can see react-native-background-upload in action and try things out in an isolated local environment. -[ReactNativeBackgroundUploadExample](https://github.com/Vydia/ReactNativeBackgroundUploadExample) +[RNBackgroundExample](https://github.com/Vydia/react-native-background-upload/blob/master/example/RNBackgroundExample) Does it support iOS camera roll assets? diff --git a/example/RNBackgroundExample/.buckconfig b/example/RNBackgroundExample/.buckconfig new file mode 100644 index 00000000..934256cb --- /dev/null +++ b/example/RNBackgroundExample/.buckconfig @@ -0,0 +1,6 @@ + +[android] + target = Google Inc.:Google APIs:23 + +[maven_repositories] + central = https://repo1.maven.org/maven2 diff --git a/example/RNBackgroundExample/.eslintrc.js b/example/RNBackgroundExample/.eslintrc.js new file mode 100644 index 00000000..40c6dcd0 --- /dev/null +++ b/example/RNBackgroundExample/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: '@react-native-community', +}; diff --git a/example/RNBackgroundExample/.flowconfig b/example/RNBackgroundExample/.flowconfig new file mode 100644 index 00000000..4afc766a --- /dev/null +++ b/example/RNBackgroundExample/.flowconfig @@ -0,0 +1,75 @@ +[ignore] +; We fork some components by platform +.*/*[.]android.js + +; Ignore "BUCK" generated dirs +/\.buckd/ + +; Ignore polyfills +node_modules/react-native/Libraries/polyfills/.* + +; These should not be required directly +; require from fbjs/lib instead: require('fbjs/lib/warning') +node_modules/warning/.* + +; Flow doesn't support platforms +.*/Libraries/Utilities/LoadingView.js + +[untyped] +.*/node_modules/@react-native-community/cli/.*/.* + +[include] + +[libs] +node_modules/react-native/Libraries/react-native/react-native-interface.js +node_modules/react-native/flow/ + +[options] +emoji=true + +esproposal.optional_chaining=enable +esproposal.nullish_coalescing=enable + +module.file_ext=.js +module.file_ext=.json +module.file_ext=.ios.js + +munge_underscores=true + +module.name_mapper='^react-native$' -> '/node_modules/react-native/Libraries/react-native/react-native-implementation' +module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' +module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FlowFixMeProps +suppress_type=$FlowFixMeState + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError + +[lints] +sketchy-null-number=warn +sketchy-null-mixed=warn +sketchy-number=warn +untyped-type-import=warn +nonstrict-import=warn +deprecated-type=warn +unsafe-getters-setters=warn +inexact-spread=warn +unnecessary-invariant=warn +signature-verification-failure=warn +deprecated-utility=error + +[strict] +deprecated-type +nonstrict-import +sketchy-null +unclear-type +unsafe-getters-setters +untyped-import +untyped-type-import + +[version] +^0.105.0 diff --git a/example/RNBackgroundExample/.gitattributes b/example/RNBackgroundExample/.gitattributes new file mode 100644 index 00000000..d42ff183 --- /dev/null +++ b/example/RNBackgroundExample/.gitattributes @@ -0,0 +1 @@ +*.pbxproj -text diff --git a/example/RNBackgroundExample/.gitignore b/example/RNBackgroundExample/.gitignore new file mode 100644 index 00000000..f69d912c --- /dev/null +++ b/example/RNBackgroundExample/.gitignore @@ -0,0 +1,61 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots + +# Bundle artifact +*.jsbundle + +artifacts/ + +# CocoaPods +/ios/Pods/ diff --git a/example/RNBackgroundExample/.prettierrc.js b/example/RNBackgroundExample/.prettierrc.js new file mode 100644 index 00000000..5c4de1a4 --- /dev/null +++ b/example/RNBackgroundExample/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + bracketSpacing: false, + jsxBracketSameLine: true, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/example/RNBackgroundExample/.watchmanconfig b/example/RNBackgroundExample/.watchmanconfig new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/example/RNBackgroundExample/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/example/RNBackgroundExample/App.js b/example/RNBackgroundExample/App.js new file mode 100644 index 00000000..0f3ea77d --- /dev/null +++ b/example/RNBackgroundExample/App.js @@ -0,0 +1,324 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + * + * @format + * @flow + */ + +import React, {useState} from 'react'; +import { + Alert, + SafeAreaView, + StyleSheet, + ScrollView, + View, + Text, + StatusBar, + Button, + Platform, + TouchableOpacity, +} from 'react-native'; + +import {Header, Colors} from 'react-native/Libraries/NewAppScreen'; + +import Upload from 'react-native-background-upload'; + +import ImagePicker from 'react-native-image-picker'; + +import RNFS from 'react-native-fs'; + +const url10SecDelayPut = 'http://localhost:8080/10secDelay'; +const url5secDelayFail = 'http://localhost:8080/5secDelayFail'; + +const path = RNFS.TemporaryDirectoryPath + '/test.json'; +const prefix = Platform.OS === 'ios' ? 'file://' : ''; + +const commonOptions = { + url: '', + path: prefix + path, + method: 'PUT', + type: 'raw', + // only supported on Android + notification: { + enabled: true, + }, +}; + +RNFS.writeFile(path, ''); + +const App: () => React$Node = () => { + const [delay10Completed, set10SecDelayCompleted] = useState(false); + const [delay5Completed, set5SecDelayCompleted] = useState(false); + + const [isImagePickerShowing, setIsImagePickerShowing] = useState(false); + const [uploadId, setUploadId] = useState(null); + const [progress, setProgress] = useState(null); + + const onPressUpload = options => { + if (isImagePickerShowing) { + return; + } + + setIsImagePickerShowing(true); + + const imagePickerOptions = { + takePhotoButtonTitle: null, + title: 'Upload Media', + chooseFromLibraryButtonTitle: 'Choose From Library', + }; + + ImagePicker.showImagePicker(imagePickerOptions, response => { + let didChooseVideo = true; + + console.log('ImagePicker response: ', response); + const {customButton, didCancel, error, path, uri} = response; + + if (didCancel) { + didChooseVideo = false; + } + + if (error) { + console.warn('ImagePicker error:', response); + didChooseVideo = false; + } + + // TODO: Should this happen higher? + setIsImagePickerShowing(false); + + if (!didChooseVideo) { + return; + } + + let finalPath = Platform.OS === 'android' ? path : uri; + + if (finalPath) { + // Video is stored locally on the device + Upload.getFileInfo(finalPath).then(metadata => { + const uploadOpts = Object.assign( + { + path: finalPath, + method: 'POST', + headers: { + 'content-type': metadata.mimeType, // server requires a content-type header + }, + }, + options, + ); + + Upload.startUpload(uploadOpts) + .then(uploadId => { + console.log( + `Upload started with options: ${JSON.stringify(uploadOpts)}`, + ); + setUploadId(uploadId); + setProgress(0); + Upload.addListener('progress', uploadId, data => { + if (data.progress % 5 === 0) { + setProgress(+data.progress); + } + console.log(`Progress: ${data.progress}%`); + }); + Upload.addListener('error', uploadId, data => { + console.log(`Error: ${data.error}%`); + }); + Upload.addListener('completed', uploadId, data => { + console.log('Completed!'); + }); + }) + .catch(function(err) { + setUploadId(null); + setProgress(null); + console.log('Upload error!', err); + }); + }); + } else { + // Video is stored in google cloud + Alert.alert('Video not found'); + } + }); + }; + + return ( + <> + + + +
+ {global.HermesInternal == null ? null : ( + + Engine: Hermes + + )} + + + { + const options = { + ...commonOptions, + url: url10SecDelayPut, + }; + + Upload.startUpload(options) + .then(uploadId => { + setUploadId(uploadId); + + Upload.addListener( + 'completed', + uploadId, + ({responseCode}) => { + console.warn({responseCode}); + + if (responseCode <= 299) { + set10SecDelayCompleted(true); + } + }, + ); + }) + .catch(err => { + console.warn(err.message); + }); + }}> + 10 Sec Delay Success + + + {delay10Completed && ( + + Finished!!! + + )} + + + { + const options = { + ...commonOptions, + url: url5secDelayFail, + }; + + Upload.startUpload(options) + .then(uploadId => { + setUploadId(uploadId); + + Upload.addListener( + 'completed', + uploadId, + ({responseCode}) => { + if (responseCode === 502) { + set5SecDelayCompleted(true); + } + }, + ); + + Upload.addListener( + 'error', + uploadId, + ({responseCode}) => { + if (responseCode === 502) { + set5SecDelayCompleted(true); + } + }, + ); + }) + .catch(err => { + console.warn(err.message); + }); + }}> + 5 Sec Delay Error + + + {delay5Completed && ( + + Finished!!! + + )} + +