diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..701c63d8 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,128 @@ +version: 2.1 + +orbs: + react-native: react-native-community/react-native@5.3.0 + +commands: + checkout-attach-workspace: + description: 'Checkout and attach workspace' + steps: + - checkout + - attach_workspace: + at: ~/app + +jobs: + install-dependencies: + executor: react-native/linux_js + working_directory: ~/app + steps: + - checkout + - react-native/yarn_install + - persist_to_workspace: + root: ~/app + paths: + - node_modules + + lint: + executor: react-native/linux_js + working_directory: ~/app + steps: + - checkout-attach-workspace + - run: + name: Lint + command: yarn lint + + validate-typescript: + executor: react-native/linux_js + working_directory: ~/app + steps: + - checkout-attach-workspace + - run: + name: Typescript + command: yarn typescript + + ios_build: + macos: + xcode: 11.5.0 + working_directory: ~/app + steps: + - checkout + - react-native/setup_macos_executor + - run: yarn install --frozen-lockfile --non-interactive + - react-native/pod_install: + pod_install_directory: ./example/ios + - react-native/ios_build: + project_path: ./example/ios/Example.xcworkspace + project_type: workspace + scheme: example + device: iPhone 11 + + publish-version: + executor: react-native/linux_js + working_directory: ~/app + steps: + - checkout-attach-workspace + - run: + name: Publish New Version + command: yarn ci:publish + + deploy-docs: + executor: + name: react-native/linux_js + node_version: '12' + working_directory: ~/app + steps: + - checkout-attach-workspace + - run: + name: Deploying new docs-version + command: | + git config --global user.email "${GH_EMAIL}@users.noreply.github.com" + git config --global user.name "${GH_NAME}" + echo "machine github.com login $GH_NAME password $GH_TOKEN" > ~/.netrc + cd website && yarn install && CUSTOM_COMMIT_MESSAGE="[skip ci]" GIT_USER=${GH_NAME} yarn deploy + +workflows: + version: 2 + + build-lint-app: + jobs: + - install-dependencies: + filters: + branches: + ignore: + - master + - react-native/android_build: + checkout: true + project_path: ./example/android + workspace_root: ~/app + filters: + branches: + ignore: + - master + - ios_build: + filters: + branches: + ignore: + - master + - lint: + requires: + - install-dependencies + - validate-typescript: + requires: + - install-dependencies + + release: + jobs: + - install-dependencies: + filters: + branches: + only: + - master + - publish-version: + requires: + - install-dependencies + - deploy-docs: + filters: + branches: + only: + - master diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..25301931 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +website/ + +# generated by bob +lib/ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..498d49a4 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + extends: ['satya164'], + + rules: { + 'react-native/no-color-literals': 'off', + + 'prettier/prettier': [ + 'error', + { + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', + useTabs: false, + printWidth: 100, + }, + ], + }, + + globals: { + __DEV__: true, + }, +}; diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..ab0a7ff9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: 🐛 Report a bug +about: Report a reproducible or regression bug. +labels: 'bug' +--- + +### Steps to reproduce +1. +2. +3. + +### Expected behaviour +Tell us what should happen + +### Actual behaviour +Tell us what happens instead + +### Environment +- **React Native version**: +- **React Native platform + platform version**: iOS 9.0, Android 5.0, etc +- **Typescript version** (if using typescript): 3.8+ required, what version is in your environment? + +### react-native-share +**Version**: npm version or "master" + +### Link to repo (highly encouraged) +https://github.com/ diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..6075473b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: ✨ Feature request +about: Suggest an idea. +labels: 'enhancement' +--- + +## Describe the Feature + + +## Possible Implementations + + +## Related Issues + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..dceea019 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,8 @@ +--- +name: 💬 Question +about: You need help with the library. +labels: 'question' +--- + +## Ask your Question + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..2ba26397 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +# Overview + + + + +# Test Plan + + diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..fba14be0 --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,7 @@ +# Configuration for request-info - https://github.com/behaviorbot/request-info + +# *Required* Comment to reply with +requestInfoReplyComment: > + We would appreciate it if you could provide us with more info about this issue/pr! :smiley + +requestInfoLabelToAdd: needs-more-info \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..247e4f60 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,19 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v3 + with: + stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. You may also mark this issue as a "discussion" and i will leave this open' + stale-pr-message: 'This pull request has been marked as stale. Thank you for your contributions. You may also comment, or remove this label in order to avoid closing this.' diff --git a/.gitignore b/.gitignore index ae2a9a38..f08bac63 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,20 @@ DerivedData *.xcuserstate project.xcworkspace +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +android/gradle/ +android/gradlew +android/gradlew.bat + +# Visual Studio +*.VC.db + # node.js # node_modules/ @@ -48,10 +62,26 @@ coverage # node-waf configuration .lock-wscript -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release +# Dependencies +/node_modules -# Dependency directory -# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git -node_modules -build/ +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# generated by bob +lib/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..250e23cc --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +example +website +.eslintrc +.eslintignore +.tsconfig.json +commitlint.config.js +CHANGELOG.md +CODEOWNERS +react-native.config.js +prettier.config.js \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..7b405f2d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,133 @@ +### Note + +Now we are using semantic-release to handle the changelog for this package and their release. You can look at the entire change-log of each release [here](https://github.com/react-native-community/react-native-share/releases). + +## 3.0.0-4 (2020-01-16) + +##### Build System / Dependencies + +- **deps:** bump eslint-utils from 1.4.0 to 1.4.2 ([#580](https://github.com/react-native-community/react-native-share/pull/580)) ([62dd4ab3](https://github.com/react-native-community/react-native-share/commit/62dd4ab3ff0832fe99f37013422d5e4d35357482)) +- use react-native-share git master as dependency for RN60 example ([b5eb9d36](https://github.com/react-native-community/react-native-share/commit/b5eb9d365bf23c8e0d8e739957466311caebbdcd)) + +##### Chores + +- **readme:** mocking with Jest example ([#610](https://github.com/react-native-community/react-native-share/pull/610)) ([796ac3a6](https://github.com/react-native-community/react-native-share/commit/796ac3a6e3dd000a3fef36bd8ead4d21483e3289)) +- **lint:** fixing lint errors ([4e78f355](https://github.com/react-native-community/react-native-share/commit/4e78f355da44abe84a790d20eddf58629758056e)) +- update example to use internal file provider, RN60 autolink works ([aa126048](https://github.com/react-native-community/react-native-share/commit/aa126048206c60291a0e3bb9024542b23abab4af)) +- use older flow package, run yarn ([bdee601d](https://github.com/react-native-community/react-native-share/commit/bdee601d3bc3ccbfc513d49f52cb1b2f2456e335)) +- fix react version to template versions ([70b0fa38](https://github.com/react-native-community/react-native-share/commit/70b0fa38d88499ae521b390eeef8cccf81ee43e9)) +- update gradle wrapper and distribution to 3.5.0/5.6.1-all ([81bf5612](https://github.com/react-native-community/react-native-share/commit/81bf5612298d4ba224705517a37518e5e00575ab)) +- update gradle in main project ([5d459e78](https://github.com/react-native-community/react-native-share/commit/5d459e7882ddbd78966f9b0018152ec2a9bbffbc)) +- update dependencies and actually port example to RN60 ([2ccc244a](https://github.com/react-native-community/react-native-share/commit/2ccc244ad4a356b0f7296a867d4cebb27d1d869c)) +- Adding es6 default export e export with destructing ([018d2a3b](https://github.com/react-native-community/react-native-share/commit/018d2a3b4e3b2e85acbe1aa664b68e4a9312b44b)) +- **ShareIntent:** indentation fix ([43fe0b51](https://github.com/react-native-community/react-native-share/commit/43fe0b51eb8697730a864f27eb9bcd4b6df60c90)) + +##### Continuous Integration + +- **fix:** adapt to new workflow ([#626](https://github.com/react-native-community/react-native-share/pull/626)) ([f706e8ca](https://github.com/react-native-community/react-native-share/commit/f706e8cae5b21079c2716998911ad86e0cd1e8ac)) + +##### Bug Fixes + +- removing lint errors ([a977d1f8](https://github.com/react-native-community/react-native-share/commit/a977d1f8bf38c0f9a37571dd2b039ef34b20db6c)) +- added missing "v" to `source` field in podspec ([#619](https://github.com/react-native-community/react-native-share/pull/619)) ([d88e542d](https://github.com/react-native-community/react-native-share/commit/d88e542ddd0983d09a4aa1a82737bb05b5731801)) +- remove uncessary tools replace on build gradle ([9ad34367](https://github.com/react-native-community/react-native-share/commit/9ad3436701f7799c47f1d861ed78cb604066fbb9)) +- instagram-stories build failure ([56f50cc9](https://github.com/react-native-community/react-native-share/commit/56f50cc9eb2ed0ccb0dfa8957cee478e47d76f74)) +- update jest to fix known security vulnerabilities ([#577](https://github.com/react-native-community/react-native-share/pull/577)) ([f6c6105b](https://github.com/react-native-community/react-native-share/commit/f6c6105b38e33de9f9af8be9c9cfc604b8eb8959)) +- remove redundant dependency ([48492907](https://github.com/react-native-community/react-native-share/commit/48492907e08764dc686877e036f0b9fb9ce6b463)) +- use the template support version ([1d7bf06d](https://github.com/react-native-community/react-native-share/commit/1d7bf06dc9618fdeec8fd17dd655f4ff1e0b3b91)) +- remove non-template gradle properties caching/parallel ([85426520](https://github.com/react-native-community/react-native-share/commit/854265201b04883042b230037b36b897494b3835)) +- .iml files are in .gitignore and should not be committed ([d7b60a50](https://github.com/react-native-community/react-native-share/commit/d7b60a50ca510b3a1d342690ca64ced720b39c84)) +- remove version from flowconfig ([5c8b2e37](https://github.com/react-native-community/react-native-share/commit/5c8b2e37a50a5b6ca5cd98bf4e8a9a8cc6edb14b)) +- social should be optional in android ([abad39db](https://github.com/react-native-community/react-native-share/commit/abad39dbbade5ca42e0f940bf3a901efdf7be771)) + +##### Other Changes + +- Resolve promise if ShareSheet is manually dismissed ([#607](https://github.com/react-native-community/react-native-share/pull/607)) ([736a8ace](https://github.com/react-native-community/react-native-share/commit/736a8ace926f0eade649c9ae516ace06c4675e22)) + +#### 1.2.1-5 (2019-05-29) + +##### Chores + +- **codeowners:** add CODEOWNERS file ([ac67e5cd](https://github.com/react-native-community/react-native-share/commit/ac67e5cd9531e5d554b7b9ac0217c777e4d8f9c4)) + +### 1.2.0-4 (2019-05-26) + +##### Build System / Dependencies + +- fix all deprecation and lint in ShareFile(s) ([#374](https://github.com/react-native-community/react-native-share/pull/374)) ([67fb59e9](https://github.com/react-native-community/react-native-share/commit/67fb59e9dc7ec9f98ad76f6809dbc98d240c451e)) + +##### Chores + +- **npmignore:** + - add .github folder to npmignore ([fc219481](https://github.com/react-native-community/react-native-share/commit/fc2194818c7dba4dd913dd5c65564113ef575a6a)) + - add changelog.js ([8256431b](https://github.com/react-native-community/react-native-share/commit/8256431be2526a55b625b72f4727d8dd8af20aee)) +- **changelog:** update changelog.js ([85a91b05](https://github.com/react-native-community/react-native-share/commit/85a91b05993eaaf5e897815029bf58d0944ed3cb)) +- Adding instructions about how use the master branch ([c1c58b87](https://github.com/react-native-community/react-native-share/commit/c1c58b876b718bce033593ad52e7fc62fdb32065)) +- update iOS target to 9.0 to match react-native min version ([f72dbe1a](https://github.com/react-native-community/react-native-share/commit/f72dbe1a44105a0d04f3af03a7556ea123024493)) + +##### New Features + +- **deps:** update deps and prepare for release ([#501](https://github.com/react-native-community/react-native-share/pull/501)) ([05c2b6a1](https://github.com/react-native-community/react-native-share/commit/05c2b6a1aeb74853ef16265f690b3ba48cd0198f)) + +#### 1.1.3-3 (2018-10-23) + +##### Build System / Dependencies + +- reverse dep order, google then jcenter ([#387](https://github.com/react-native-community/react-native-share/pull/387)) ([2c91ecce](https://github.com/react-native-community/react-native-share/commit/2c91ecceda3abe182fa500a6bcd2b09e0b5fd4e5)) +- upgrade android dependencies, example depend on upstream ([#373](https://github.com/react-native-community/react-native-share/pull/373)) ([28e62b15](https://github.com/react-native-community/react-native-share/commit/28e62b1526b2242a474b9b7f3a4dd213d2ec3554)) + +##### Bug Fixes + +- **classes-not-exported:** fix classes not exported warning ([540aa8fe](https://github.com/react-native-community/react-native-share/commit/540aa8fe68ede1ac4bfa79698dabe78448b59cc3)) + +#### 1.1.2-2 (2018-09-12) + +#### 1.1.1-1 (2018-08-07) + +##### New Features + +- **issue-template:** add issue template ([fde759f8](https://github.com/react-native-community/react-native-share/commit/fde759f8412687d7a70a1fca1a848839fb57df51)) + +#### 1.1.0 (2018-07-25) + +##### Build System / Dependencies + +- **idx:** add idx as dev dep and improve flow ([eba00817](https://github.com/react-native-community/react-native-share/commit/eba008177c0157f606c14fb13305039dc3058576)) + +##### Chores + +- **readme:** update readme with circle ci status badge ([1a789ffe](https://github.com/react-native-community/react-native-share/commit/1a789ffe51f73a50775d49da1687dbe677faae18)) + +##### New Features + +- **README:** add pagesmanager only android ([cb206d64](https://github.com/react-native-community/react-native-share/commit/cb206d643913c292b668a8b651580c83a77ccfd7)) +- **gradle-3:** + - fix circle script ([5c20929d](https://github.com/react-native-community/react-native-share/commit/5c20929d8fb51fbe08a2bef7d39fd5cb985f39aa)) + - rollback compile and add gradlew clean to circle ([c4bece4f](https://github.com/react-native-community/react-native-share/commit/c4bece4f9a16624a9ef357a5aa7162563ae684ac)) + - rollback to compile ([32217f7f](https://github.com/react-native-community/react-native-share/commit/32217f7f60d9fc8dd07b924b928f8070f30f6b68)) + - fix build.gradle ([6544b5b5](https://github.com/react-native-community/react-native-share/commit/6544b5b5263a337a5c64df3cedd73bf98ae6d6c1)) + - add gradle ([73ed033d](https://github.com/react-native-community/react-native-share/commit/73ed033debdd7f76e53e87866512e57a60dd2fc3)) +- **circl-flow-eslint:** + - fix circle ci yml file name ([2983b13a](https://github.com/react-native-community/react-native-share/commit/2983b13abf61ad3baf9bf7e98074cc4890ea1d87)) + - fix circle ci yml file name ([7d78542d](https://github.com/react-native-community/react-native-share/commit/7d78542d2f563e068cba7f515fa6eed2d56a6406)) +- **circle-flow-eslint:** first atempt add circle, add flow, eslint and prettier ([44ac820e](https://github.com/react-native-community/react-native-share/commit/44ac820e77bc90f331490320a509a52d630272b2)) +- **social:** add social facebook pages manager ([2537d3fe](https://github.com/react-native-community/react-native-share/commit/2537d3fe8104972014716535ffcdbc3157cb56c0)) +- update format ([a32ed6cd](https://github.com/react-native-community/react-native-share/commit/a32ed6cd5a13b90293c6e79c239b16085be52104)) +- update readme for url format ([dda13853](https://github.com/react-native-community/react-native-share/commit/dda138536f5d7f27236e95274698cb2766fbfdfe)) + +##### Bug Fixes + +- **changelog:** fallback changelog script ([b635de04](https://github.com/react-native-community/react-native-share/commit/b635de044ce3931a85156fe3c5a23b67c98f0317)) +- **flow:** use Node and add CHANGELOG.md ([ed6fcd32](https://github.com/react-native-community/react-native-share/commit/ed6fcd32d398968de7b07dafb6d736ade10525d7)) +- **social:** rename file FacebookPagesManager to FacebookPagesManagerShare ([ac25bcad](https://github.com/react-native-community/react-native-share/commit/ac25bcad51a2ae9e0b1b2658de66d1cc06ab83f6)) + +##### Other Changes + +- version ([3f01bb15](https://github.com/react-native-community/react-native-share/commit/3f01bb15f45f684ece157cb00b5c1c10383975ba)) +- version ([a708692c](https://github.com/react-native-community/react-native-share/commit/a708692ca18102f84c7012edc4c7460a131cef92)) +- version ([77f4d80a](https://github.com/react-native-community/react-native-share/commit/77f4d80acc750a0edd179e85c3e4e9847c42dca6)) +- google plus sharing android ([7c6a65f7](https://github.com/react-native-community/react-native-share/commit/7c6a65f76819020d55ca6d95320b0cb1d0060849)) +- version 1.0.23 ([9a9f94dd](https://github.com/react-native-community/react-native-share/commit/9a9f94ddbf5a33c9d7afba669dfae73773fe86b2)) +- version ([672d6962](https://github.com/react-native-community/react-native-share/commit/672d6962c006e9b52a0a889ce9ff958734f84070)) +- email share ([1faeac79](https://github.com/react-native-community/react-native-share/commit/1faeac7914bf1437a7c289bd3922de4fd5e35db4)) +- local files shared in android ([797dc89c](https://github.com/react-native-community/react-native-share/commit/797dc89cb7368011ccda74c4b7ff585186e9304a)) diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..4e82ebed --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @jgcmarins @MateusAndrade @mikehardy diff --git a/README.md b/README.md index e150abf5..7abf4bc8 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,55 @@ -# react-native-share [![npm version](https://badge.fury.io/js/react-native-share.svg)](http://badge.fury.io/js/react-native-share) -Share Social , Sending Simple Data to Other Apps - -## Getting started - -### Mostly automatic install -1. `npm install rnpm --global` -2. `npm install react-native-share --save` -3. `rnpm link react-native-share` - -### Manual install - -#### iOS - -1. `npm install react-native-share --save` -2. In XCode, in the project navigator, right click `Libraries` ➜ `Add Files to [your project's name]` -3. Go to `node_modules` ➜ `react-native-share` and add `RNShare.xcodeproj` -4. In XCode, in the project navigator, select your project. Add `libRNShare.a` to your project's `Build Phases` ➜ `Link Binary With Libraries` -5. Run your project (`Cmd+R`) - -#### Android - -1. `npm install react-native-share --save` -2. Open up `android/app/src/main/java/[...]/MainActivity.java` - - Add `import cl.json.RNSharePackage;` to the imports at the top of the file - - Add `new RNSharePackage()` to the list returned by the `getPackages()` method -3. Append the following lines to `android/settings.gradle`: - ``` - include ':react-native-share' - project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android') - ``` -4. Insert the following lines inside the dependencies block in `android/app/build.gradle`: - - ``` - compile project(':react-native-share') - ``` - -#### Windows -[Read it! :D](https://github.com/ReactWindows/react-native) - -1. `npm install react-native-share --save` -2. In Visual Studio add the `RNShare.sln` in `node_modules/react-native-share/windows/RNShare.sln` folder to their solution, reference from their app. -2. Open up your `MainPage.cs` app - - Add `using Cl.Json.RNShare;` to the usings at the top of the file - - Add `new RNSharePackage()` to the `List` returned by the `Packages` method - - -## Usage - -```javascript -import React, { - AppRegistry, - Component, - StyleSheet, - Text, - View, - TouchableHighlight -} from 'react-native'; +# react-native-share [![react-native-share](https://circleci.com/gh/react-native-share/react-native-share.svg?style=svg)](https://app.circleci.com/pipelines/github/react-native-share/react-native-share) [![npm version](https://badge.fury.io/js/react-native-share.svg)](http://badge.fury.io/js/react-native-share) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) + +React Native Share, a simple tool for share message and file to other apps. + +# Sponsors + +--- + +If you use this library on your commercial/personal projects, you can help us by funding the work on specific issues that you choose by using IssueHunt.io! + +This gives you the power to prioritize our work and support the project contributors. Moreover it'll guarantee the project will be updated and maintained in the long run. + +[![issuehunt-image](https://issuehunt.io/static/embed/issuehunt-button-v1.svg)](https://issuehunt.io/repos/43406976) + +# Getting started + +--- + +If you are using `react-native >= 0.60` you just need to do a simple: + +```shell +yarn add react-native-share +``` + +Or if are using npm: + +```shell +npm i react-native-share --save +``` + +After that, we need to install the dependencies to use the project on iOS(you can skip this part, if you are using this on Android). + +Now run a simple: `npx pod-install` or `cd ios && pod install`. After that, you should be able to use the library on both Platforms, iOS and Android. + +Then simply import: + +```js import Share from 'react-native-share'; -class Example extends Component { - onShare() { - Share.open({ - share_text: "Hola mundo", - share_URL: "http://google.cl", - title: "Share Link" - },(e) => { - console.log(e); - }); - } - render() { - return ( - - - Welcome to React Native! - - - To get started, edit index.ios.js - - - Press Cmd+R to reload,{'\n'} - Cmd+D or shake for dev menu - - - - Social Share - - - - ); - } -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#F5FCFF', - }, - welcome: { - fontSize: 20, - textAlign: 'center', - margin: 10, - }, - instructions: { - textAlign: 'center', - color: '#333333', - marginBottom: 5, - }, -}); - -AppRegistry.registerComponent('Example', () => Example); +Share.open(options) + .then((res) => { + console.log(res); + }) + .catch((err) => { + err && console.log(err); + }); ``` -## how it looks: -![Demo Android](/assets/android.png) -![Demo iOS](/assets/ios.png) -![Demo Windows](/assets/windows.png) +Which you do something similar to this: + +![example-ios](website/static/img/assets-docs/ios-readme-example.gif) + +# Documentation + +If you are using a older version of `react-native` or `react-native-share`, having any problem or want to know how use `Share.open` and other functions, please refer to our new [docs](https://react-native-share.github.io/react-native-share) and help us improve that. 🚀 diff --git a/RNShare.podspec b/RNShare.podspec new file mode 100644 index 00000000..32750f0d --- /dev/null +++ b/RNShare.podspec @@ -0,0 +1,18 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "RNShare" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.author = { package["author"]["name"] => package["author"]["email"] } + s.platform = :ios, "8.0" + s.source = { :git => "https://github.com/react-native-community/react-native-share.git", :tag => "v#{s.version}" } + + s.source_files = "ios/**/*.{h,m}" + + s.dependency "React-Core" +end diff --git a/android/.npmignore b/android/.npmignore new file mode 100644 index 00000000..54b25fb5 --- /dev/null +++ b/android/.npmignore @@ -0,0 +1,10 @@ +*.iml +.DS_Store +.gradle/ +.idea/ +.npmignore +build/ +gradle/ +gradlew +gradlew.bat +local.properties diff --git a/android/build.gradle b/android/build.gradle index 45d29cd7..6aa215d0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,23 +1,51 @@ +def safeExtGet(prop, fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback +} + +buildscript { + // The Android Gradle plugin is only required when opening the android folder stand-alone. + // This avoids unnecessary downloads and potential conflicts when the library is included as a + // module dependency in an application project. + if (project == rootProject) { + repositories { + mavenCentral() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.5.2' + } + } +} + apply plugin: 'com.android.library' android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - + compileSdkVersion safeExtGet('compileSdkVersion', 28) defaultConfig { - minSdkVersion 16 - targetSdkVersion 22 - versionCode 1 - versionName "1.0" - ndk { - abiFilters "armeabi-v7a", "x86" - } + minSdkVersion safeExtGet('minSdkVersion', 16) + targetSdkVersion safeExtGet('targetSdkVersion', 28) } lintOptions { - warning 'InvalidPackage' + abortOnError false + warning 'InvalidPackage' + } +} + +repositories { + mavenLocal() + mavenCentral() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url "$rootDir/../node_modules/react-native/android" + } + maven { + // Android JSC is installed from npm + url "$rootDir/../node_modules/jsc-android/dist" } + google() } dependencies { - compile 'com.facebook.react:react-native:0.20.+' + //noinspection GradleDynamicVersion + implementation 'com.facebook.react:react-native:+' // From node_modules } diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..5c2d1cf0 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ca9d6281 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 00000000..83f2acfd --- /dev/null +++ b/android/gradlew @@ -0,0 +1,188 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 00000000..24467a14 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 44ad448a..dd0b5ce8 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,4 +1,15 @@ + package="cl.json"> + + + + + diff --git a/android/src/main/java/cl/json/RNShareFileProvider.java b/android/src/main/java/cl/json/RNShareFileProvider.java new file mode 100644 index 00000000..462e584e --- /dev/null +++ b/android/src/main/java/cl/json/RNShareFileProvider.java @@ -0,0 +1,7 @@ +package cl.json; + +import androidx.core.content.FileProvider; + +public class RNShareFileProvider extends FileProvider { + +} diff --git a/android/src/main/java/cl/json/RNShareModule.java b/android/src/main/java/cl/json/RNShareModule.java index 0cacfa01..b0dfd518 100644 --- a/android/src/main/java/cl/json/RNShareModule.java +++ b/android/src/main/java/cl/json/RNShareModule.java @@ -1,88 +1,215 @@ package cl.json; -import android.content.Intent; +import android.app.Activity; import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.Nullable; +import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.Callback; -public class RNShareModule extends ReactContextBaseJavaModule { - - private final ReactApplicationContext reactContext; - - public RNShareModule(ReactApplicationContext reactContext) { - super(reactContext); - this.reactContext = reactContext; - } +import java.util.HashMap; +import java.util.Map; + +import cl.json.social.EmailShare; +import cl.json.social.FacebookShare; +import cl.json.social.FacebookStoriesShare; +import cl.json.social.FacebookPagesManagerShare; +import cl.json.social.GenericShare; +import cl.json.social.GooglePlusShare; +import cl.json.social.ShareIntent; +import cl.json.social.TargetChosenReceiver; +import cl.json.social.TelegramShare; +import cl.json.social.TwitterShare; +import cl.json.social.WhatsAppShare; +import cl.json.social.WhatsAppBusinessShare; +import cl.json.social.InstagramShare; +import cl.json.social.InstagramStoriesShare; +import cl.json.social.PinterestShare; +import cl.json.social.SnapChatShare; +import cl.json.social.SMSShare; +import cl.json.social.MessengerShare; +import cl.json.social.LinkedinShare; + +public class RNShareModule extends ReactContextBaseJavaModule implements ActivityEventListener { + + public static final int SHARE_REQUEST_CODE = 16845; + private final ReactApplicationContext reactContext; + + // removed @Override temporarily just to get it working on different versions of RN + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == SHARE_REQUEST_CODE && resultCode == Activity.RESULT_CANCELED) { + TargetChosenReceiver.sendCallback(true, false, "CANCELED"); + } + } - @Override - public String getName() { - return "RNShare"; - } + // removed @Override temporarily just to get it working on different versions of RN + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + onActivityResult(requestCode, resultCode, data); + } - @ReactMethod - public void open(ReadableMap options, Callback callback) { - Intent shareIntent = createShareIntent(options); - Intent intentChooser = createIntentChooser(options, shareIntent); + @Override + public void onNewIntent(Intent intent) { - try { - this.reactContext.startActivity(intentChooser); - callback.invoke("OK"); - } catch (ActivityNotFoundException ex) { - callback.invoke("not_available"); } - } - - /** - * Creates an {@link Intent} to be shared from a set of {@link ReadableMap} options - * @param {@link ReadableMap} options - * @return {@link Intent} intent - */ - private Intent createShareIntent(ReadableMap options) { - Intent intent = new Intent(android.content.Intent.ACTION_SEND); - intent.setType("text/plain"); - - if (hasValidKey("share_text", options)) { - intent.putExtra(Intent.EXTRA_SUBJECT, options.getString("share_text")); + + private enum SHARES { + facebook, + facebookstories, + generic, + pagesmanager, + twitter, + whatsapp, + whatsappbusiness, + instagram, + instagramstories, + googleplus, + email, + pinterest, + messenger, + snapchat, + sms, + linkedin, + telegram; + + + public static ShareIntent getShareClass(String social, ReactApplicationContext reactContext) { + SHARES share = valueOf(social); + switch (share) { + case generic: + return new GenericShare(reactContext); + case facebook: + return new FacebookShare(reactContext); + case facebookstories: + return new FacebookStoriesShare(reactContext); + case pagesmanager: + return new FacebookPagesManagerShare(reactContext); + case twitter: + return new TwitterShare(reactContext); + case whatsapp: + return new WhatsAppShare(reactContext); + case whatsappbusiness: + return new WhatsAppBusinessShare(reactContext); + case instagram: + return new InstagramShare(reactContext); + case instagramstories: + return new InstagramStoriesShare(reactContext); + case googleplus: + return new GooglePlusShare(reactContext); + case email: + return new EmailShare(reactContext); + case pinterest: + return new PinterestShare(reactContext); + case sms: + return new SMSShare(reactContext); + case snapchat: + return new SnapChatShare(reactContext); + case messenger: + return new MessengerShare(reactContext); + case linkedin: + return new LinkedinShare(reactContext); + case telegram: + return new TelegramShare(reactContext); + default: + return null; + } + } + }; + + public RNShareModule(ReactApplicationContext reactContext) { + super(reactContext); + reactContext.addActivityEventListener(this); + this.reactContext = reactContext; } - if (hasValidKey("share_URL", options)) { - intent.putExtra(Intent.EXTRA_TEXT, options.getString("share_URL")); + @Override + public String getName() { + return "RNShare"; } - return intent; - } - - /** - * Creates an {@link Intent} representing an intent chooser - * @param {@link ReadableMap} options - * @param {@link Intent} intent to share - * @return {@link Intent} intent - */ - private Intent createIntentChooser(ReadableMap options, Intent intent) { - String title = "Share"; - if (hasValidKey("title", options)) { - title = options.getString("title"); + @javax.annotation.Nullable + @Override + public Map getConstants() { + Map constants = new HashMap<>(); + for (SHARES val : SHARES.values()) { + constants.put(val.toString().toUpperCase(), val.toString()); + } + return constants; } - Intent chooser = Intent.createChooser(intent, title); - chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + @ReactMethod + public void open(ReadableMap options, @Nullable Callback failureCallback, @Nullable Callback successCallback) { + TargetChosenReceiver.registerCallbacks(successCallback, failureCallback); + try { + GenericShare share = new GenericShare(this.reactContext); + share.open(options); + } catch (ActivityNotFoundException ex) { + System.out.println("ERROR " + ex.getMessage()); + ex.printStackTrace(System.out); + TargetChosenReceiver.sendCallback(false, "not_available"); + } catch (Exception e) { + System.out.println("ERROR " + e.getMessage()); + e.printStackTrace(System.out); + TargetChosenReceiver.sendCallback(false, e.getMessage()); + } + } - return chooser; - } + @ReactMethod + public void shareSingle(ReadableMap options, @Nullable Callback failureCallback, @Nullable Callback successCallback) { + System.out.println("SHARE SINGLE METHOD"); + TargetChosenReceiver.registerCallbacks(successCallback, failureCallback); + if (ShareIntent.hasValidKey("social", options)) { + try { + ShareIntent shareClass = SHARES.getShareClass(options.getString("social"), this.reactContext); + if (shareClass != null && shareClass instanceof ShareIntent) { + shareClass.open(options); + } else { + throw new ActivityNotFoundException("Invalid share activity"); + } + } catch (ActivityNotFoundException ex) { + System.out.println("ERROR " + ex.getMessage()); + ex.printStackTrace(System.out); + TargetChosenReceiver.sendCallback(false, ex.getMessage()); + } catch (Exception e) { + System.out.println("ERROR " + e.getMessage()); + e.printStackTrace(System.out); + TargetChosenReceiver.sendCallback(false, e.getMessage()); + } + } else { + TargetChosenReceiver.sendCallback(false, "key 'social' missing in options"); + } + } - /** - * Checks if a given key is valid - * @param @{link String} key - * @param @{link ReadableMap} options - * @return boolean representing whether the key exists and has a value - */ - private boolean hasValidKey(String key, ReadableMap options) { - return options.hasKey(key) && !options.isNull(key); - } + @ReactMethod + public void isPackageInstalled(String packagename, @Nullable Callback failureCallback, @Nullable Callback successCallback) { + try { + boolean res = ShareIntent.isPackageInstalled(packagename, this.reactContext); + successCallback.invoke(res); + } catch (Exception e) { + System.out.println("Error: " + e.getMessage()); + failureCallback.invoke(e.getMessage()); + } + } -} \ No newline at end of file + @ReactMethod + public void isBase64File(String url, @Nullable Callback failureCallback, @Nullable Callback successCallback) { + try { + Uri uri = Uri.parse(url); + String scheme = uri.getScheme(); + if ((scheme != null) && scheme.equals("data")) { + successCallback.invoke(true); + } else { + successCallback.invoke(false); + } + } catch (Exception e) { + System.out.println("ERROR " + e.getMessage()); + e.printStackTrace(System.out); + failureCallback.invoke(e.getMessage()); + } + } +} diff --git a/android/src/main/java/cl/json/RNSharePackage.java b/android/src/main/java/cl/json/RNSharePackage.java index ae7af713..201d8ca8 100644 --- a/android/src/main/java/cl/json/RNSharePackage.java +++ b/android/src/main/java/cl/json/RNSharePackage.java @@ -15,7 +15,7 @@ public List createNativeModules(ReactApplicationContext reactConte return Arrays.asList(new RNShareModule(reactContext)); } - @Override + // Deprecated from RN 0.47.0 public List> createJSModules() { return Collections.emptyList(); } diff --git a/android/src/main/java/cl/json/RNSharePathUtil.java b/android/src/main/java/cl/json/RNSharePathUtil.java new file mode 100644 index 00000000..20304351 --- /dev/null +++ b/android/src/main/java/cl/json/RNSharePathUtil.java @@ -0,0 +1,202 @@ +package cl.json; + +import android.app.Application; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import androidx.annotation.NonNull; +import androidx.loader.content.CursorLoader; +import androidx.core.content.FileProvider; +import android.text.TextUtils; + +import com.facebook.react.bridge.ReactContext; + +import java.io.File; +import java.util.ArrayList; + +public class RNSharePathUtil { + private static final ArrayList authorities = new ArrayList<>(); + + public static void compileAuthorities(ReactContext reactContext) { + if (authorities.size() == 0) { + Application application = (Application) reactContext.getApplicationContext(); + if (application instanceof ShareApplication) { + authorities.add(((ShareApplication) application).getFileProviderAuthority()); + } + + authorities.add(reactContext.getPackageName() + ".rnshare.fileprovider"); + } + } + + public static Uri compatUriFromFile(@NonNull final ReactContext context, @NonNull final File file) { + compileAuthorities(context); + String existingAuthority = Uri.fromFile(file).getAuthority(); + + // Authority is already set on this uri, no need to set it again + if (!TextUtils.isEmpty(existingAuthority) && authorities.contains(existingAuthority)) { + return Uri.fromFile(file); + } + + // Already a content uri, cannot set authority on this + if (file.getAbsolutePath().startsWith("content://")) { + return Uri.fromFile(file); + } + + // No authority present, getting FileProvider uri + Uri result = null; + for (int i = 0; i < authorities.size(); i++) { + try { + String authority = authorities.get(i); + result = FileProvider.getUriForFile(context, authority, file); + if (result != null) { + break; + } + } catch (Exception e) { + System.out.println("RNSharePathUtil::compatUriFromFile ERROR " + e.getMessage()); + } + } + return result; + } + + public static String getRealPathFromURI(final Context context, final Uri uri) { + + String filePrefix = ""; + // DocumentProvider + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type) || "0".equalsIgnoreCase(type)) { + return filePrefix + context.getExternalCacheDir() + "/" + split[1]; + } else if ("raw".equalsIgnoreCase(type)) { + return filePrefix + split[1]; + } else if (!TextUtils.isEmpty(type)) { + return filePrefix + "/storage/" + type + "/" + split[1]; + } + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + if (id.startsWith("raw:")) { + return filePrefix + id.replaceFirst("raw:", ""); + } + final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return filePrefix + getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } else if ("raw".equalsIgnoreCase(type)) { + return filePrefix + split[1]; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{ + split[1] + }; + + return filePrefix + getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + + return filePrefix + getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + Cursor cursor = null; + final String column = MediaStore.MediaColumns.DATA; + final String[] projection = { column }; + + try { + CursorLoader loader = new CursorLoader(context, uri, projection, selection, selectionArgs, null); + cursor = loader.loadInBackground(); + if (cursor != null && cursor.moveToFirst()) { + final int index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(index); + } + } finally { + if (cursor != null) cursor.close(); + } + return null; + } + + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + public static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + +} diff --git a/android/src/main/java/cl/json/ShareApplication.java b/android/src/main/java/cl/json/ShareApplication.java new file mode 100644 index 00000000..0e31d1fa --- /dev/null +++ b/android/src/main/java/cl/json/ShareApplication.java @@ -0,0 +1,7 @@ +package cl.json; + +public interface ShareApplication { + + public String getFileProviderAuthority(); + +} diff --git a/android/src/main/java/cl/json/ShareFile.java b/android/src/main/java/cl/json/ShareFile.java new file mode 100644 index 00000000..e3312faa --- /dev/null +++ b/android/src/main/java/cl/json/ShareFile.java @@ -0,0 +1,149 @@ +package cl.json; + +import android.net.Uri; +import android.os.Environment; +import android.util.Base64; +import android.webkit.MimeTypeMap; + +import com.facebook.react.bridge.ReactApplicationContext; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Created by disenodosbbcl on 22-07-16. + */ +public class ShareFile { + + public static final int BASE_64_DATA_LENGTH = 5; // `data:` + public static final int BASE_64_DATA_OFFSET = 8; // `;base64,` + private final ReactApplicationContext reactContext; + private String url; + private Uri uri; + private String type; + private String filename; + + public ShareFile(String url, String type, String filename, ReactApplicationContext reactContext){ + this(url, filename, reactContext); + this.type = type; + this.filename = filename; + } + + public ShareFile(String url, String filename, ReactApplicationContext reactContext){ + this.url = url; + this.uri = Uri.parse(this.url); + this.reactContext = reactContext; + this.filename = filename; + } + /** + * Obtain mime type from URL + * @param url {@link String} + * @return {@link String} mime type + */ + private String getMimeType(String url) { + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + return type; + } + /** + * Return an if the url is a file (local or base64)l + * @return {@link boolean} + */ + public boolean isFile() { + return this.isBase64File() || this.isLocalFile(); + } + + private boolean isBase64File() { + String scheme = uri.getScheme(); + if((scheme != null) && uri.getScheme().equals("data")) { + StringBuilder type = new StringBuilder(); + char[] parts = this.uri.toString().substring(BASE_64_DATA_LENGTH).toCharArray(); + for (char part : parts) { + if (part == ';') { + break; + } + type.append(part); + } + + this.type = type.toString(); + return true; + } + return false; + } + + private boolean isLocalFile() { + String scheme = uri.getScheme(); + if((scheme != null) && (uri.getScheme().equals("content") || uri.getScheme().equals("file"))) { + // type is already set + if (this.type != null) { + return true; + } + // try to get mimetype from uri + this.type = this.getMimeType(uri.toString()); + + // try resolving the file and get the mimetype + if(this.type == null) { + String realPath = this.getRealPathFromURI(uri); + if (realPath != null) { + this.type = this.getMimeType(realPath); + } else { + return false; + } + } + + if(this.type == null) { + this.type = "*/*"; + } + + return true; + } + return false; + } + public String getType() { + if (this.type == null) { + return "*/*"; + } + return this.type; + } + private String getRealPathFromURI(Uri contentUri) { + String result = RNSharePathUtil.getRealPathFromURI(this.reactContext, contentUri); + return result; + } + public Uri getURI() { + + final MimeTypeMap mime = MimeTypeMap.getSingleton(); + String extension = mime.getExtensionFromMimeType(getType()); + + if(this.isBase64File()) { + String encodedImg = this.uri.toString().substring(BASE_64_DATA_LENGTH + this.type.length() + BASE_64_DATA_OFFSET); + String filename = this.filename != null ? this.filename : System.nanoTime() + ""; + try { + File dir = new File(this.reactContext.getExternalCacheDir(), Environment.DIRECTORY_DOWNLOADS ); + if (!dir.exists() && !dir.mkdirs()) { + throw new IOException("mkdirs failed on " + dir.getAbsolutePath()); + } + File file = new File(dir, filename + "." + extension); + final FileOutputStream fos = new FileOutputStream(file); + fos.write(Base64.decode(encodedImg, Base64.DEFAULT)); + fos.flush(); + fos.close(); + return RNSharePathUtil.compatUriFromFile(reactContext, file); + + } catch (IOException e) { + e.printStackTrace(); + } + } else if(this.isLocalFile()) { + Uri uri = Uri.parse(this.url); + if (uri.getPath() == null) { + return null; + } + return RNSharePathUtil.compatUriFromFile(reactContext, new File(uri.getPath())); + } + + return null; + } +} diff --git a/android/src/main/java/cl/json/ShareFiles.java b/android/src/main/java/cl/json/ShareFiles.java new file mode 100644 index 00000000..67933d2b --- /dev/null +++ b/android/src/main/java/cl/json/ShareFiles.java @@ -0,0 +1,170 @@ +package cl.json; + +import android.net.Uri; +import android.os.Environment; +import android.util.Base64; +import android.webkit.MimeTypeMap; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; + +/** + * Created by bhavesh on 11/08/17. + */ + +public class ShareFiles +{ + private final ReactApplicationContext reactContext; + private ArrayList uris; + private ArrayList filenames; + private String intentType; + + public ShareFiles(ReadableArray urls, ArrayList filenames, String type, ReactApplicationContext reactContext) { + this(urls, filenames, reactContext); + this.intentType = type; + } + + public ShareFiles(ReadableArray urls, ArrayList filenames, ReactApplicationContext reactContext) { + this.uris = new ArrayList<>(); + for (int i = 0; i < urls.size(); i++) { + String url = urls.getString(i); + if (url != null) { + Uri uri = Uri.parse(url); + this.uris.add(uri); + } + } + this.filenames = filenames; + this.reactContext = reactContext; + } + /** + * Obtain mime type from URL + * @param url {@link String} + * @return {@link String} mime type + */ + private String getMimeType(String url) { + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + return type; + } + /** + * Return an if the url is a file (local or base64)l + * @return {@link boolean} + */ + public boolean isFile() { + boolean isFile = true; + for (Uri uri : this.uris) { + isFile = this.isBase64File(uri) || this.isLocalFile(uri); + if (!isFile) { + break; + } + } + return isFile; + } + + private boolean isBase64File(Uri uri) { + String scheme = uri.getScheme(); + if((scheme != null) && uri.getScheme().equals("data")) { + String type = uri.getSchemeSpecificPart().substring(0, uri.getSchemeSpecificPart().indexOf(";")); + if (this.intentType == null) { + this.intentType = type; + } else if (!this.intentType.equalsIgnoreCase(type) && this.intentType.split("/")[0].equalsIgnoreCase((type.split("/"))[0])) { + this.intentType = (this.intentType.split("/")[0]).concat("/*"); + } else if (!this.intentType.equalsIgnoreCase(type)) { + this.intentType = "*/*"; + } + return true; + } + return false; + } + private boolean isLocalFile(Uri uri) { + String scheme = uri.getScheme(); + if((scheme != null) && uri.getScheme().equals("content") || "file".equals(uri.getScheme())) { +// // type is already set +// if (this.type != null) { +// return true; +// } + // try to get mimetype from uri + String type = this.getMimeType(uri.toString()); + + // try resolving the file and get the mimetype + if(type == null) { + String realPath = this.getRealPathFromURI(uri); + type = this.getMimeType(realPath); + } + if(type == null) { + type = "*/*"; + } + + if (this.intentType == null) { + this.intentType = type; + } else if (!this.intentType.equalsIgnoreCase(type) && this.intentType.split("/")[0].equalsIgnoreCase((type.split("/"))[0])) { + this.intentType = (this.intentType.split("/")[0]).concat("/*"); + } else if (!this.intentType.equalsIgnoreCase(type)) { + this.intentType = "*/*"; + } + + return true; + } + return false; + } + + public String getType() { + if (this.intentType == null) { + return "*/*"; + } + return this.intentType; + } + + private String getRealPathFromURI(Uri contentUri) { + String result = RNSharePathUtil.getRealPathFromURI(this.reactContext, contentUri); + return result; + } + + public ArrayList getURI() { + final MimeTypeMap mime = MimeTypeMap.getSingleton(); + ArrayList finalUris = new ArrayList<>(); + + for (int uriIndex = 0; uriIndex < this.uris.size(); uriIndex++) { + Uri uri = this.uris.get(uriIndex); + + if(this.isBase64File(uri)) { + String type = uri.getSchemeSpecificPart().substring(0, uri.getSchemeSpecificPart().indexOf(";")); + String extension = mime.getExtensionFromMimeType(type); + String encodedImg = uri.getSchemeSpecificPart().substring(uri.getSchemeSpecificPart().indexOf(";base64,") + 8); + String fileName = filenames.size() >= uriIndex + 1 ? filenames.get(uriIndex) : (System.currentTimeMillis() + "." + extension); + try { + File dir = new File(this.reactContext.getExternalCacheDir(), Environment.DIRECTORY_DOWNLOADS ); + if (!dir.exists() && !dir.mkdirs()) { + throw new IOException("mkdirs failed on " + dir.getAbsolutePath()); + } + File file = new File(dir, fileName); + final FileOutputStream fos = new FileOutputStream(file); + fos.write(Base64.decode(encodedImg, Base64.DEFAULT)); + fos.flush(); + fos.close(); + finalUris.add(RNSharePathUtil.compatUriFromFile(reactContext, file)); + } catch (IOException e) { + e.printStackTrace(); + } + } else if(this.isLocalFile(uri)) { + if (uri.getPath() != null) { + if (filenames.size() >= uriIndex + 1) { + finalUris.add(RNSharePathUtil.compatUriFromFile(reactContext, new File(uri.getPath(), filenames.get(uriIndex)))); + } else { + finalUris.add(RNSharePathUtil.compatUriFromFile(reactContext, new File(uri.getPath()))); + } + } + } + } + + return finalUris; + } +} diff --git a/android/src/main/java/cl/json/social/EmailShare.java b/android/src/main/java/cl/json/social/EmailShare.java new file mode 100644 index 00000000..2015bd2e --- /dev/null +++ b/android/src/main/java/cl/json/social/EmailShare.java @@ -0,0 +1,39 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by disenodosbbcl on 23-07-16. + */ +public class EmailShare extends SingleShareIntent { + + private static final String PACKAGE = "com.google.android.gm"; + + public EmailShare(ReactApplicationContext reactContext) { + super(reactContext); + } + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return null; + } +} + diff --git a/android/src/main/java/cl/json/social/FacebookPagesManagerShare.java b/android/src/main/java/cl/json/social/FacebookPagesManagerShare.java new file mode 100644 index 00000000..21d142fd --- /dev/null +++ b/android/src/main/java/cl/json/social/FacebookPagesManagerShare.java @@ -0,0 +1,39 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by disenodosbbcl on 23-07-16. + */ +public class FacebookPagesManagerShare extends SingleShareIntent { + + private static final String PACKAGE = "com.facebook.pages.app"; + private static final String DEFAULT_WEB_LINK = "https://www.facebook.com/sharer/sharer.php?u={url}"; + + public FacebookPagesManagerShare(ReactApplicationContext reactContext) { + super(reactContext); + } + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // MORE DATA + this.openIntentChooser(); + } + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return DEFAULT_WEB_LINK; + } + + @Override + protected String getPlayStoreLink() { + return null; + } +} diff --git a/android/src/main/java/cl/json/social/FacebookShare.java b/android/src/main/java/cl/json/social/FacebookShare.java new file mode 100644 index 00000000..50991a6e --- /dev/null +++ b/android/src/main/java/cl/json/social/FacebookShare.java @@ -0,0 +1,40 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by disenodosbbcl on 23-07-16. + */ +public class FacebookShare extends SingleShareIntent { + + private static final String PACKAGE = "com.facebook.katana"; + private static final String DEFAULT_WEB_LINK = "https://www.facebook.com/sharer/sharer.php?u={url}"; + + public FacebookShare(ReactApplicationContext reactContext) { + super(reactContext); + + } + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // MORE DATA + this.openIntentChooser(); + } + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return DEFAULT_WEB_LINK; + } + + @Override + protected String getPlayStoreLink() { + return null; + } +} diff --git a/android/src/main/java/cl/json/social/FacebookStoriesShare.java b/android/src/main/java/cl/json/social/FacebookStoriesShare.java new file mode 100644 index 00000000..17f3c9bd --- /dev/null +++ b/android/src/main/java/cl/json/social/FacebookStoriesShare.java @@ -0,0 +1,114 @@ +package cl.json.social; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.os.Environment; +import android.net.Uri; +import java.io.File; + +import cl.json.ShareFile; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by Kaio Duarte on 23-12-20. + */ +public class FacebookStoriesShare extends SingleShareIntent { + + private static final String PACKAGE = "com.facebook.katana"; + private static final String PLAY_STORE_LINK = "market://details?id=com.facebook.katana"; + + public FacebookStoriesShare(ReactApplicationContext reactContext) { + super(reactContext); + this.setIntent(new Intent("com.facebook.stories.ADD_TO_STORY")); + } + + @Override + public void open(ReadableMap options) throws ActivityNotFoundException, IllegalArgumentException { + super.open(options); + this.shareStory(options); + // extra params here + this.openIntentChooser(options); + } + + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } + + private void shareStory(ReadableMap options) { + if (!this.hasValidKey("appId", options)) { + throw new IllegalArgumentException("appId was not provided."); + } + + if (!this.hasValidKey("backgroundImage", options) && !this.hasValidKey("backgroundVideo", options) + && !this.hasValidKey("stickerImage", options)) { + throw new IllegalArgumentException("Invalid background or sticker assets provided."); + } + + Activity activity = this.reactContext.getCurrentActivity(); + + if (activity == null) { + TargetChosenReceiver.sendCallback(false, "Something went wrong"); + return; + } + + this.intent.putExtra("com.facebook.platform.extra.APPLICATION_ID", options.getString("appId")); + this.intent.putExtra("bottom_background_color", "#906df4"); + this.intent.putExtra("top_background_color", "#837DF4"); + + if (this.hasValidKey("attributionURL", options)) { + this.intent.putExtra("content_url", options.getString("attributionURL")); + } + + if (this.hasValidKey("backgroundTopColor", options)) { + this.intent.putExtra("top_background_color", options.getString("backgroundTopColor")); + } + + if (this.hasValidKey("backgroundBottomColor", options)) { + this.intent.putExtra("bottom_background_color", options.getString("backgroundBottomColor")); + } + + Boolean hasBackgroundAsset = this.hasValidKey("backgroundImage", options) + || this.hasValidKey("backgroundVideo", options); + + if (hasBackgroundAsset) { + String backgroundFileName = ""; + + if (this.hasValidKey("backgroundImage", options)) { + backgroundFileName = options.getString("backgroundImage"); + } else if (this.hasValidKey("backgroundVideo", options)) { + backgroundFileName = options.getString("backgroundVideo"); + } + + ShareFile backgroundAsset = new ShareFile(backgroundFileName, "background", this.reactContext); + + this.intent.setDataAndType(backgroundAsset.getURI(), backgroundAsset.getType()); + this.intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + if (this.hasValidKey("stickerImage", options)) { + ShareFile stickerAsset = new ShareFile(options.getString("stickerImage"), "sticker", this.reactContext); + + if (!hasBackgroundAsset) { + this.intent.setType("image/*"); + } + + this.intent.putExtra("interactive_asset_uri", stickerAsset.getURI()); + activity.grantUriPermission("com.facebook.katana", stickerAsset.getURI(), + Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } +} diff --git a/android/src/main/java/cl/json/social/GenericShare.java b/android/src/main/java/cl/json/social/GenericShare.java new file mode 100644 index 00000000..c3432a79 --- /dev/null +++ b/android/src/main/java/cl/json/social/GenericShare.java @@ -0,0 +1,37 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by disenodosbbcl on 23-07-16. + */ +public class GenericShare extends ShareIntent { + public GenericShare(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + + @Override + protected String getPackage() { + return null; + } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return null; + } +} diff --git a/android/src/main/java/cl/json/social/GooglePlusShare.java b/android/src/main/java/cl/json/social/GooglePlusShare.java new file mode 100644 index 00000000..3d1d81bd --- /dev/null +++ b/android/src/main/java/cl/json/social/GooglePlusShare.java @@ -0,0 +1,42 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by disenodosbbcl on 23-07-16. + */ +public class GooglePlusShare extends SingleShareIntent { + + private static final String PACKAGE = "com.google.android.apps.plus"; + private static final String DEFAULT_WEB_LINK = "https://plus.google.com/share?url={url}"; + private static final String PLAY_STORE_LINK = "market://details?id=com.google.android.apps.plus"; + + public GooglePlusShare(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return DEFAULT_WEB_LINK; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } +} diff --git a/android/src/main/java/cl/json/social/InstagramShare.java b/android/src/main/java/cl/json/social/InstagramShare.java new file mode 100644 index 00000000..94d8a726 --- /dev/null +++ b/android/src/main/java/cl/json/social/InstagramShare.java @@ -0,0 +1,45 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import java.io.File; +import android.os.Environment; +import android.net.Uri; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by Ralf Nieuwenhuizen on 10-04-17. + */ +public class InstagramShare extends SingleShareIntent { + + private static final String PACKAGE = "com.instagram.android"; + private static final String PLAY_STORE_LINK = "https://play.google.com/store/apps/details?id=com.instagram.android"; + + public InstagramShare(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } +} diff --git a/android/src/main/java/cl/json/social/InstagramStoriesShare.java b/android/src/main/java/cl/json/social/InstagramStoriesShare.java new file mode 100644 index 00000000..6f557a23 --- /dev/null +++ b/android/src/main/java/cl/json/social/InstagramStoriesShare.java @@ -0,0 +1,109 @@ +package cl.json.social; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import java.io.File; +import android.os.Environment; +import android.net.Uri; + +import cl.json.ShareFile; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by Vladimir Stalmakov on 01-06-20. + */ +public class InstagramStoriesShare extends SingleShareIntent { + + private static final String PACKAGE = "com.instagram.android"; + private static final String PLAY_STORE_LINK = "https://play.google.com/store/apps/details?id=com.instagram.android"; + + public InstagramStoriesShare(ReactApplicationContext reactContext) { + super(reactContext); + this.setIntent(new Intent("com.instagram.share.ADD_TO_STORY")); + } + + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + this.shareStory(options); + // extra params here + this.openIntentChooser(options); + } + + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } + + private void shareStory(ReadableMap options) { + if (!this.hasValidKey("backgroundImage", options) && !this.hasValidKey("backgroundVideo", options) + && !this.hasValidKey("stickerImage", options)) { + throw new IllegalArgumentException("Invalid background or sticker assets provided."); + } + + Activity activity = this.reactContext.getCurrentActivity(); + + if (activity == null) { + TargetChosenReceiver.sendCallback(false, "Something went wrong"); + return; + } + + this.intent.putExtra("bottom_background_color", "#906df4"); + this.intent.putExtra("top_background_color", "#837DF4"); + + if (this.hasValidKey("attributionURL", options)) { + this.intent.putExtra("content_url", options.getString("attributionURL")); + } + + if (this.hasValidKey("backgroundTopColor", options)) { + this.intent.putExtra("top_background_color", options.getString("backgroundTopColor")); + } + + if (this.hasValidKey("backgroundBottomColor", options)) { + this.intent.putExtra("bottom_background_color", options.getString("backgroundBottomColor")); + } + + Boolean hasBackgroundAsset = this.hasValidKey("backgroundImage", options) + || this.hasValidKey("backgroundVideo", options); + + if (hasBackgroundAsset) { + String backgroundFileName = ""; + + if (this.hasValidKey("backgroundImage", options)) { + backgroundFileName = options.getString("backgroundImage"); + } else if (this.hasValidKey("backgroundVideo", options)) { + backgroundFileName = options.getString("backgroundVideo"); + } + + ShareFile backgroundAsset = new ShareFile(backgroundFileName, "background", this.reactContext); + + this.intent.setDataAndType(backgroundAsset.getURI(), backgroundAsset.getType()); + this.intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + if (this.hasValidKey("stickerImage", options)) { + ShareFile stickerAsset = new ShareFile(options.getString("stickerImage"), "sticker", this.reactContext); + + if (!hasBackgroundAsset) { + this.intent.setType("image/*"); + } + + this.intent.putExtra("interactive_asset_uri", stickerAsset.getURI()); + activity.grantUriPermission(InstagramStoriesShare.PACKAGE, stickerAsset.getURI(), + Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } +} diff --git a/android/src/main/java/cl/json/social/LinkedinShare.java b/android/src/main/java/cl/json/social/LinkedinShare.java new file mode 100644 index 00000000..1e4a4653 --- /dev/null +++ b/android/src/main/java/cl/json/social/LinkedinShare.java @@ -0,0 +1,45 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import java.io.File; +import android.os.Environment; +import android.net.Uri; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by Malai Mihai on 31-05-19. + */ +public class LinkedinShare extends SingleShareIntent { + + private static final String PACKAGE = "com.linkedin.android"; + private static final String PLAY_STORE_LINK = "market://details?id=com.linkedin.android"; + + public LinkedinShare(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } +} diff --git a/android/src/main/java/cl/json/social/MessengerShare.java b/android/src/main/java/cl/json/social/MessengerShare.java new file mode 100644 index 00000000..dfd91421 --- /dev/null +++ b/android/src/main/java/cl/json/social/MessengerShare.java @@ -0,0 +1,45 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import java.io.File; +import android.os.Environment; +import android.net.Uri; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by Muhzi4u on 14-01-19. + */ +public class MessengerShare extends SingleShareIntent { + + private static final String PACKAGE = "com.facebook.orca"; + private static final String PLAY_STORE_LINK = "market://details?id=com.facebook.orca"; + + public MessengerShare(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } +} diff --git a/android/src/main/java/cl/json/social/PinterestShare.java b/android/src/main/java/cl/json/social/PinterestShare.java new file mode 100644 index 00000000..4affa78a --- /dev/null +++ b/android/src/main/java/cl/json/social/PinterestShare.java @@ -0,0 +1,46 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import java.io.File; +import android.os.Environment; +import android.net.Uri; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by Deathart on 25-09-18. + */ +public class PinterestShare extends SingleShareIntent { + + private static final String PACKAGE = "com.pinterest"; + private static final String PLAY_STORE_LINK = "market://details?id=com.pinterest"; + private static final String DEFAULT_WEB_LINK = "https://pinterest.com/pin/create/button/?url={url}&media=$media&description={message}"; + + public PinterestShare(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return DEFAULT_WEB_LINK; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } +} diff --git a/android/src/main/java/cl/json/social/SMSShare.java b/android/src/main/java/cl/json/social/SMSShare.java new file mode 100644 index 00000000..40fcabfd --- /dev/null +++ b/android/src/main/java/cl/json/social/SMSShare.java @@ -0,0 +1,52 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import java.io.File; +import android.os.Environment; +import android.net.Uri; +import android.provider.Telephony; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by Muhzi4u on 14-01-19. + */ +public class SMSShare extends SingleShareIntent { + + private static final String PACKAGE = "com.android.mms"; + private static final String PLAY_STORE_LINK = "market://details?id=com.android.mms"; + + private ReactApplicationContext reactContext = null; + + public SMSShare(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; + } + + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + + @Override + protected String getPackage() { + if (android.os.Build.VERSION.SDK_INT >= 19 ) { + return Telephony.Sms.getDefaultSmsPackage(this.reactContext); + } + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } +} diff --git a/android/src/main/java/cl/json/social/ShareIntent.java b/android/src/main/java/cl/json/social/ShareIntent.java new file mode 100644 index 00000000..37a6edb8 --- /dev/null +++ b/android/src/main/java/cl/json/social/ShareIntent.java @@ -0,0 +1,335 @@ +package cl.json.social; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Parcelable; +import android.text.TextUtils; +import android.content.pm.ResolveInfo; +import android.content.ComponentName; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.net.URLEncoder; +import java.util.ArrayList; + +import cl.json.RNShareModule; +import cl.json.ShareFile; +import cl.json.ShareFiles; + +/** + * Created by disenodosbbcl on 23-07-16. + */ +public abstract class ShareIntent { + + protected final ReactApplicationContext reactContext; + protected Intent intent; + protected String chooserTitle = "Share"; + protected ShareFile fileShare; + protected ReadableMap options; + protected ShareFile stickerAsset; + protected ShareFile backgroundAsset; + + public ShareIntent(ReactApplicationContext reactContext) { + this.reactContext = reactContext; + this.setIntent(new Intent(android.content.Intent.ACTION_SEND)); + this.getIntent().setType("text/plain"); + } + + public Intent excludeChooserIntent(Intent prototype, ReadableMap options) { + List targetedShareIntents = new ArrayList(); + List> intentMetaInfo = new ArrayList>(); + Intent chooserIntent; + + Intent dummy = new Intent(prototype.getAction()); + dummy.setType(prototype.getType()); + List resInfo = this.reactContext.getPackageManager().queryIntentActivities(dummy, 0); + + if (!resInfo.isEmpty()) { + for (ResolveInfo resolveInfo : resInfo) { + if (resolveInfo.activityInfo == null || options.getArray("excludedActivityTypes").toString().contains(resolveInfo.activityInfo.packageName)) + continue; + + HashMap info = new HashMap(); + info.put("packageName", resolveInfo.activityInfo.packageName); + info.put("className", resolveInfo.activityInfo.name); + info.put("simpleName", String.valueOf(resolveInfo.activityInfo.loadLabel(this.reactContext.getPackageManager()))); + intentMetaInfo.add(info); + } + + if (!intentMetaInfo.isEmpty()) { + // sorting for nice readability + Collections.sort(intentMetaInfo, new Comparator>() { + @Override + public int compare(HashMap map, HashMap map2) { + return map.get("simpleName").compareTo(map2.get("simpleName")); + } + }); + + // create the custom intent list + for (HashMap metaInfo : intentMetaInfo) { + Intent targetedShareIntent = (Intent) prototype.clone(); + targetedShareIntent.setPackage(metaInfo.get("packageName")); + targetedShareIntent.setClassName(metaInfo.get("packageName"), metaInfo.get("className")); + targetedShareIntents.add(targetedShareIntent); + } + + chooserIntent = Intent.createChooser(targetedShareIntents.remove(targetedShareIntents.size() - 1), "share"); + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedShareIntents.toArray(new Parcelable[]{})); + return chooserIntent; + } + } + + return Intent.createChooser(prototype, "Share"); + } + + public void open(ReadableMap options) throws ActivityNotFoundException { + this.options = options; + + if (ShareIntent.hasValidKey("subject", options)) { + this.getIntent().putExtra(Intent.EXTRA_SUBJECT, options.getString("subject")); + } + + if (ShareIntent.hasValidKey("email", options)) { + this.getIntent().putExtra(Intent.EXTRA_EMAIL, new String[] { options.getString("email") }); + } + + if (ShareIntent.hasValidKey("title", options)) { + this.chooserTitle = options.getString("title"); + } + + String message = ""; + if (ShareIntent.hasValidKey("message", options)) { + message = options.getString("message"); + } + + String socialType = ""; + if (ShareIntent.hasValidKey("social", options)) { + socialType = options.getString("social"); + } + + if (socialType.equals("sms")) { + String recipient = options.getString("recipient"); + + if (!recipient.isEmpty()) { + this.getIntent().putExtra("address", recipient); + } + } + + if (socialType.equals("whatsapp")) { + if (options.hasKey("whatsAppNumber")) { + String whatsAppNumber = options.getString("whatsAppNumber"); + String chatAddress = whatsAppNumber + "@s.whatsapp.net"; + this.getIntent().putExtra("jid", chatAddress); + } + } + + if (socialType.equals("whatsappbusiness")) { + if (options.hasKey("whatsAppNumber")) { + String whatsAppNumber = options.getString("whatsAppNumber"); + String chatAddress = whatsAppNumber + "@s.whatsapp.net"; + this.getIntent().putExtra("jid", chatAddress); + } + } + + if (ShareIntent.hasValidKey("urls", options)) { + + ShareFiles fileShare = getFileShares(options); + if (fileShare.isFile()) { + ArrayList uriFile = fileShare.getURI(); + this.getIntent().setAction(Intent.ACTION_SEND_MULTIPLE); + this.getIntent().setType(fileShare.getType()); + this.getIntent().putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriFile); + this.getIntent().addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (!TextUtils.isEmpty(message)) { + this.getIntent().putExtra(Intent.EXTRA_TEXT, message); + } + } else { + if (!TextUtils.isEmpty(message)) { + this.getIntent().putExtra(Intent.EXTRA_TEXT, message + " " + options.getArray("urls").getString(0)); + } else { + this.getIntent().putExtra(Intent.EXTRA_TEXT, options.getArray("urls").getString(0)); + } + } + } else if (ShareIntent.hasValidKey("url", options)) { + this.fileShare = getFileShare(options); + if (this.fileShare.isFile()) { + Uri uriFile = this.fileShare.getURI(); + this.getIntent().setType(this.fileShare.getType()); + this.getIntent().putExtra(Intent.EXTRA_STREAM, uriFile); + this.getIntent().addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (!TextUtils.isEmpty(message)) { + this.getIntent().putExtra(Intent.EXTRA_TEXT, message); + } + } else { + if (!TextUtils.isEmpty(message)) { + this.getIntent().putExtra(Intent.EXTRA_TEXT, message + " " + options.getString("url")); + } else { + this.getIntent().putExtra(Intent.EXTRA_TEXT, options.getString("url")); + } + } + } else if (!TextUtils.isEmpty(message)) { + this.getIntent().putExtra(Intent.EXTRA_TEXT, message); + } + } + + protected ShareFile getFileShare(ReadableMap options) { + String filename = null; + if (ShareIntent.hasValidKey("filename", options)) { + filename = options.getString("filename"); + } + if (ShareIntent.hasValidKey("type", options)) { + return new ShareFile(options.getString("url"), options.getString("type"), filename, this.reactContext); + } else { + return new ShareFile(options.getString("url"), filename, this.reactContext); + } + } + + protected ShareFiles getFileShares(ReadableMap options) { + ArrayList filenames = new ArrayList<>(); + if (ShareIntent.hasValidKey("filenames", options)) { + ReadableArray fileNamesReadableArray = options.getArray("filenames"); + for (int i = 0; i < fileNamesReadableArray.size(); i++) { + filenames.add(fileNamesReadableArray.getString(i)); + } + } + + if (ShareIntent.hasValidKey("type", options)) { + return new ShareFiles(options.getArray("urls"), filenames, options.getString("type"), this.reactContext); + } else { + return new ShareFiles(options.getArray("urls"), filenames, this.reactContext); + } + } + + protected static String urlEncode(String param) { + try { + return URLEncoder.encode(param, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("URLEncoder.encode() failed for " + param); + } + } + + protected Intent[] getIntentsToViewFile(Intent intent, Uri uri) { + PackageManager pm = this.reactContext.getPackageManager(); + + List resInfo = pm.queryIntentActivities(intent, 0); + Intent[] extraIntents = new Intent[resInfo.size()]; + for (int i = 0; i < resInfo.size(); i++) { + ResolveInfo ri = resInfo.get(i); + String packageName = ri.activityInfo.packageName; + + Intent newIntent = new Intent(); + newIntent.setComponent(new ComponentName(packageName, ri.activityInfo.name)); + newIntent.setAction(Intent.ACTION_VIEW); + newIntent.setDataAndType(uri, intent.getType()); + newIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + extraIntents[i] = new Intent(newIntent); + } + + return extraIntents; + } + + protected void openIntentChooser() throws ActivityNotFoundException { + Activity activity = this.reactContext.getCurrentActivity(); + if (activity == null) { + TargetChosenReceiver.sendCallback(false, "Something went wrong"); + return; + } + Intent chooser; + IntentSender intentSender = null; + if (TargetChosenReceiver.isSupported()) { + intentSender = TargetChosenReceiver.getSharingSenderIntent(this.reactContext); + chooser = Intent.createChooser(this.getIntent(), this.chooserTitle, intentSender); + } else { + chooser = Intent.createChooser(this.getIntent(), this.chooserTitle); + } + chooser.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + + if (ShareIntent.hasValidKey("showAppsToView", options) && ShareIntent.hasValidKey("url", options)) { + Intent viewIntent = new Intent(Intent.ACTION_VIEW); + viewIntent.setType(this.fileShare.getType()); + + Intent[] viewIntents = this.getIntentsToViewFile(viewIntent, this.fileShare.getURI()); + + chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, viewIntents); + } + + if (ShareIntent.hasValidKey("excludedActivityTypes", options)) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + chooser.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, getExcludedComponentArray(options.getArray("excludedActivityTypes"))); + activity.startActivityForResult(chooser, RNShareModule.SHARE_REQUEST_CODE); + } else { + activity.startActivityForResult(excludeChooserIntent(this.getIntent(),options), RNShareModule.SHARE_REQUEST_CODE); + } + } else { + activity.startActivityForResult(chooser, RNShareModule.SHARE_REQUEST_CODE); + } + + if (intentSender == null) { + TargetChosenReceiver.sendCallback(true, true, "OK"); + } + } + + public static boolean isPackageInstalled(String packagename, Context context) { + PackageManager pm = context.getPackageManager(); + try { + pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + protected Intent getIntent() { + return this.intent; + } + + protected void setIntent(Intent intent) { + this.intent = intent; + } + + public static boolean hasValidKey(String key, ReadableMap options) { + return options != null && options.hasKey(key) && !options.isNull(key); + } + + protected abstract String getPackage(); + + protected String getComponentClass() { + return null; + } + + protected abstract String getDefaultWebLink(); + + protected abstract String getPlayStoreLink(); + + private ComponentName[] getExcludedComponentArray(ReadableArray excludeActivityTypes){ + if (excludeActivityTypes == null){ + return null; + } + Intent dummy = new Intent(getIntent().getAction()); + dummy.setType(getIntent().getType()); + List componentNameList = new ArrayList<>(); + List resInfoList = this.reactContext.getPackageManager().queryIntentActivities(dummy, 0); + for (int index = 0; index < excludeActivityTypes.size(); index++) { + String packageName = excludeActivityTypes.getString(index); + for(ResolveInfo resInfo : resInfoList) { + if(resInfo.activityInfo.packageName.equals(packageName)) { + componentNameList.add(new ComponentName(resInfo.activityInfo.packageName, resInfo.activityInfo.name)); + } + } + } + return componentNameList.toArray(new ComponentName[]{}); + } +} diff --git a/android/src/main/java/cl/json/social/SingleShareIntent.java b/android/src/main/java/cl/json/social/SingleShareIntent.java new file mode 100644 index 00000000..dd0fae9f --- /dev/null +++ b/android/src/main/java/cl/json/social/SingleShareIntent.java @@ -0,0 +1,95 @@ +package cl.json.social; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.net.Uri; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +import cl.json.RNShareModule; + +/** + * Created by disenodosbbcl on 23-07-16. + */ +public abstract class SingleShareIntent extends ShareIntent { + + protected String playStoreURL = null; + protected String appStoreURL = null; + + public SingleShareIntent(ReactApplicationContext reactContext) { + super(reactContext); + } + + public void open(ReadableMap options) throws ActivityNotFoundException { + System.out.println(getPackage()); + // check if package is installed + if (getPackage() != null || getDefaultWebLink() != null || getPlayStoreLink() != null) { + if (this.isPackageInstalled(getPackage(), reactContext)) { + System.out.println("INSTALLED"); + if (getComponentClass() != null) { + ComponentName cn = new ComponentName(getPackage(), getComponentClass()); + this.getIntent().setComponent(cn); + } else { + this.getIntent().setPackage(getPackage()); + } + super.open(options); + return; // once we open we don't need to continue + } else { + System.out.println("NOT INSTALLED"); + String url = ""; + if (getDefaultWebLink() != null) { + url = getDefaultWebLink() + .replace("{url}", this.urlEncode(options.getString("url"))) + .replace("{message}", this.urlEncode(options.getString("message"))); + } else if (getPlayStoreLink() != null) { + url = getPlayStoreLink(); + } else { + // TODO + } + // open web intent + this.setIntent(new Intent(new Intent("android.intent.action.VIEW", Uri.parse(url)))); + } + } + // configure default + super.open(options); + } + + protected void openIntentChooser() throws ActivityNotFoundException { + this.openIntentChooser(null); + } + + protected void openIntentChooser(ReadableMap options) throws ActivityNotFoundException { + if (this.options.hasKey("forceDialog") && this.options.getBoolean("forceDialog")) { + Activity activity = this.reactContext.getCurrentActivity(); + if (activity == null) { + TargetChosenReceiver.sendCallback(false, "Something went wrong"); + return; + } + if (options != null) { + if (!ShareIntent.hasValidKey("social", options)) { + throw new IllegalArgumentException("social is empty"); + } + } + if (TargetChosenReceiver.isSupported()) { + IntentSender sender = TargetChosenReceiver.getSharingSenderIntent(this.reactContext); + Intent chooser = Intent.createChooser(this.getIntent(), this.chooserTitle, sender); + chooser.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + activity.startActivityForResult(chooser, RNShareModule.SHARE_REQUEST_CODE); + } else { + Intent chooser = Intent.createChooser(this.getIntent(), this.chooserTitle); + chooser.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + activity.startActivityForResult(chooser, RNShareModule.SHARE_REQUEST_CODE); + TargetChosenReceiver.sendCallback(true, true, "OK"); + } + } else { + this.getIntent().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + this.reactContext.startActivity(this.getIntent()); + TargetChosenReceiver.sendCallback(true, true, this.getIntent().getPackage()); + } + } +} diff --git a/android/src/main/java/cl/json/social/SnapChatShare.java b/android/src/main/java/cl/json/social/SnapChatShare.java new file mode 100644 index 00000000..a68217df --- /dev/null +++ b/android/src/main/java/cl/json/social/SnapChatShare.java @@ -0,0 +1,49 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import java.io.File; +import android.os.Environment; +import android.net.Uri; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by Muhzi4u on 14-01-19. + */ +public class SnapChatShare extends SingleShareIntent { + + private static final String PACKAGE = "com.snapchat.android"; + private static final String CLASS = "com.snapchat.android.LandingPageActivity"; + private static final String PLAY_STORE_LINK = "market://details?id=com.snapchat.android"; + + public SnapChatShare(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getComponentClass() { return CLASS; } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } +} diff --git a/android/src/main/java/cl/json/social/TargetChosenReceiver.java b/android/src/main/java/cl/json/social/TargetChosenReceiver.java new file mode 100644 index 00000000..648388b9 --- /dev/null +++ b/android/src/main/java/cl/json/social/TargetChosenReceiver.java @@ -0,0 +1,94 @@ +package cl.json.social; + +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentSender; +import android.os.Build; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactContext; + +/** + * Receiver to record the chosen component when sharing an Intent. + */ +public class TargetChosenReceiver extends BroadcastReceiver { + private static final String EXTRA_RECEIVER_TOKEN = "receiver_token"; + private static final Object LOCK = new Object(); + + private static String sTargetChosenReceiveAction; + private static TargetChosenReceiver sLastRegisteredReceiver; + + private static Callback successCallback; + private static Callback failureCallback; + + public static boolean isSupported() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1; + } + + public static void registerCallbacks(Callback success, Callback failure) { + successCallback = success; + failureCallback = failure; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) + public static IntentSender getSharingSenderIntent(ReactContext reactContext) { + synchronized (LOCK) { + if (sTargetChosenReceiveAction == null) { + sTargetChosenReceiveAction = reactContext.getPackageName() + "/" + TargetChosenReceiver.class.getName() + "_ACTION"; + } + Context context = reactContext.getApplicationContext(); + if (sLastRegisteredReceiver != null) { + context.unregisterReceiver(sLastRegisteredReceiver); + } + sLastRegisteredReceiver = new TargetChosenReceiver(); + context.registerReceiver(sLastRegisteredReceiver, new IntentFilter(sTargetChosenReceiveAction)); + } + + Intent intent = new Intent(sTargetChosenReceiveAction); + intent.setPackage(reactContext.getPackageName()); + intent.setClass(reactContext.getApplicationContext(), TargetChosenReceiver.class); + intent.putExtra(EXTRA_RECEIVER_TOKEN, sLastRegisteredReceiver.hashCode()); + final PendingIntent callback = PendingIntent.getBroadcast(reactContext, 0, intent, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE : + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); + + return callback.getIntentSender(); + } + + @Override + public void onReceive(Context context, Intent intent) { + synchronized (LOCK) { + if (sLastRegisteredReceiver != this) return; + context.getApplicationContext().unregisterReceiver(sLastRegisteredReceiver); + sLastRegisteredReceiver = null; + } + if (!intent.hasExtra(EXTRA_RECEIVER_TOKEN) || intent.getIntExtra(EXTRA_RECEIVER_TOKEN, 0) != this.hashCode()) { + return; + } + + ComponentName target = intent.getParcelableExtra(Intent.EXTRA_CHOSEN_COMPONENT); + if (target != null) { + sendCallback(true, true, target.flattenToString()); + } else { + sendCallback(true, true, "OK"); + } + } + + public static void sendCallback(boolean isSuccess, Object... reply) { + if (isSuccess) { + if (successCallback != null) { + successCallback.invoke(reply); + } + } else if (failureCallback != null) { + failureCallback.invoke(reply); + } + successCallback = null; + failureCallback = null; + } +} diff --git a/android/src/main/java/cl/json/social/TelegramShare.java b/android/src/main/java/cl/json/social/TelegramShare.java new file mode 100644 index 00000000..9f5f4038 --- /dev/null +++ b/android/src/main/java/cl/json/social/TelegramShare.java @@ -0,0 +1,40 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by Harish on 21-07-2021. + */ +public class TelegramShare extends SingleShareIntent { + + private static final String PACKAGE = "org.telegram.messenger"; + private static final String PLAY_STORE_LINK = "market://details?id=org.telegram.messenger"; + + public TelegramShare(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } +} diff --git a/android/src/main/java/cl/json/social/TwitterShare.java b/android/src/main/java/cl/json/social/TwitterShare.java new file mode 100644 index 00000000..78d7ff4c --- /dev/null +++ b/android/src/main/java/cl/json/social/TwitterShare.java @@ -0,0 +1,39 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by disenodosbbcl on 23-07-16. + */ +public class TwitterShare extends SingleShareIntent { + + private static final String PACKAGE = "com.twitter.android"; + private static final String DEFAULT_WEB_LINK = "https://twitter.com/intent/tweet?text={message}&url={url}"; + + public TwitterShare(ReactApplicationContext reactContext) { + super(reactContext); + } + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return DEFAULT_WEB_LINK; + } + + @Override + protected String getPlayStoreLink() { + return null; + } +} diff --git a/android/src/main/java/cl/json/social/WhatsAppBusinessShare.java b/android/src/main/java/cl/json/social/WhatsAppBusinessShare.java new file mode 100644 index 00000000..97079cbd --- /dev/null +++ b/android/src/main/java/cl/json/social/WhatsAppBusinessShare.java @@ -0,0 +1,39 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by vijay(vijay@gemsessence.com) on 07-June-2021. + */ +public class WhatsAppBusinessShare extends SingleShareIntent { + + private static final String PACKAGE = "com.whatsapp.w4b"; + private static final String PLAY_STORE_LINK = "market://details?id=com.whatsapp.w4b"; + + public WhatsAppBusinessShare(ReactApplicationContext reactContext) { + super(reactContext); + } + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } +} diff --git a/android/src/main/java/cl/json/social/WhatsAppShare.java b/android/src/main/java/cl/json/social/WhatsAppShare.java new file mode 100644 index 00000000..30993ee5 --- /dev/null +++ b/android/src/main/java/cl/json/social/WhatsAppShare.java @@ -0,0 +1,39 @@ +package cl.json.social; + +import android.content.ActivityNotFoundException; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; + +/** + * Created by disenodosbbcl on 23-07-16. + */ +public class WhatsAppShare extends SingleShareIntent { + + private static final String PACKAGE = "com.whatsapp"; + private static final String PLAY_STORE_LINK = "market://details?id=com.whatsapp"; + + public WhatsAppShare(ReactApplicationContext reactContext) { + super(reactContext); + } + @Override + public void open(ReadableMap options) throws ActivityNotFoundException { + super.open(options); + // extra params here + this.openIntentChooser(); + } + @Override + protected String getPackage() { + return PACKAGE; + } + + @Override + protected String getDefaultWebLink() { + return null; + } + + @Override + protected String getPlayStoreLink() { + return PLAY_STORE_LINK; + } +} diff --git a/android/src/main/res/xml/share_download_paths.xml b/android/src/main/res/xml/share_download_paths.xml new file mode 100644 index 00000000..47bce316 --- /dev/null +++ b/android/src/main/res/xml/share_download_paths.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 00000000..acbff84e --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,30 @@ +module.exports = { + rules: { + 'body-leading-blank': [1, 'always'], + 'footer-leading-blank': [1, 'always'], + 'header-max-length': [2, 'always', 80], + 'scope-case': [2, 'always', 'lower-case'], + 'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']], + 'subject-empty': [2, 'never'], + 'subject-full-stop': [2, 'never', '.'], + 'type-case': [2, 'always', 'lower-case'], + 'type-empty': [2, 'never'], + 'type-enum': [ + 2, + 'always', + [ + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test', + ], + ], + }, +}; diff --git a/example/.eslintrc.js b/example/.eslintrc.js new file mode 100644 index 00000000..40c6dcd0 --- /dev/null +++ b/example/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: '@react-native-community', +}; diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 00000000..ad572e63 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,59 @@ +# 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 + +# CocoaPods +/ios/Pods/ diff --git a/example/.prettierrc.js b/example/.prettierrc.js new file mode 100644 index 00000000..5c4de1a4 --- /dev/null +++ b/example/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + bracketSpacing: false, + jsxBracketSameLine: true, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/example/App.js b/example/App.js new file mode 100644 index 00000000..391b3264 --- /dev/null +++ b/example/App.js @@ -0,0 +1,309 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + * + * @format + * @flow + */ + +import React, {useState} from 'react'; +import { + Alert, + Button, + Platform, + TextInput, + StyleSheet, + Text, + View, +} from 'react-native'; + +import Share from 'react-native-share'; + +import images from './images/imagesBase64'; +import pdfBase64 from './images/pdfBase64'; + +const App = () => { + const [packageSearch, setPackageSearch] = useState(''); + const [recipient, setRecipient] = useState(''); + const [result, setResult] = useState(''); + + /** + * You can use the method isPackageInstalled to find if a package is installed. + * It returns an object { isInstalled, message }. + * Only works on Android. + */ + const checkIfPackageIsInstalled = async () => { + const {isInstalled} = await Share.isPackageInstalled(packageSearch); + + Alert.alert( + `Package: ${packageSearch}`, + `${isInstalled ? 'Installed' : 'Not Installed'}`, + ); + }; + + function getErrorString(error, defaultValue) { + let e = defaultValue || 'Something went wrong. Please try again'; + if (typeof error === 'string') { + e = error; + } else if (error && error.message) { + e = error.message; + } else if (error && error.props) { + e = error.props; + } + return e; + } + + /** + * This functions share multiple images that + * you send as the urls param + */ + const shareMultipleImages = async () => { + const shareOptions = { + title: 'Share file', + failOnCancel: false, + urls: [images.image1, images.image2], + }; + + // If you want, you can use a try catch, to parse + // the share response. If the user cancels, etc. + try { + const ShareResponse = await Share.open(shareOptions); + setResult(JSON.stringify(ShareResponse, null, 2)); + } catch (error) { + console.log('Error =>', error); + setResult('error: '.concat(getErrorString(error))); + } + }; + + /** + * This functions share a image passed using the + * url param + */ + const shareEmailImage = async () => { + const shareOptions = { + title: 'Share file', + email: 'email@example.com', + social: Share.Social.EMAIL, + failOnCancel: false, + urls: [images.image1, images.image2], + }; + + try { + const ShareResponse = await Share.open(shareOptions); + setResult(JSON.stringify(ShareResponse, null, 2)); + } catch (error) { + console.log('Error =>', error); + setResult('error: '.concat(getErrorString(error))); + } + }; + + /** + * This functions share a image passed using the + * url param + */ + const shareSingleImage = async () => { + const shareOptions = { + title: 'Share file', + url: images.image1, + failOnCancel: false, + }; + + try { + const ShareResponse = await Share.open(shareOptions); + setResult(JSON.stringify(ShareResponse, null, 2)); + } catch (error) { + console.log('Error =>', error); + setResult('error: '.concat(getErrorString(error))); + } + }; + + /** + * This function shares PDF and PNG files to + * the Files app that you send as the urls param + */ + const shareToFiles = async () => { + const shareOptions = { + title: 'Share file', + failOnCancel: false, + saveToFiles: true, + urls: [images.image1, images.pdf1], // base64 with mimeType or path to local file + }; + + // If you want, you can use a try catch, to parse + // the share response. If the user cancels, etc. + try { + const ShareResponse = await Share.open(shareOptions); + setResult(JSON.stringify(ShareResponse, null, 2)); + } catch (error) { + console.log('Error =>', error); + setResult('error: '.concat(getErrorString(error))); + } + }; + + const shareToInstagramStory = async () => { + const shareOptions = { + title: 'Share image to instastory', + backgroundImage: images.image1, + social: Share.Social.INSTAGRAM_STORIES, + }; + + try { + const ShareResponse = await Share.shareSingle(shareOptions); + setResult(JSON.stringify(ShareResponse, null, 2)); + } catch (error) { + console.log('Error =>', error); + setResult('error: '.concat(getErrorString(error))); + } + }; + + const shareSms = async () => { + const shareOptions = { + title: '', + social: Share.Social.SMS, + recipient, + message: 'Example SMS', + }; + + try { + const ShareResponse = await Share.shareSingle(shareOptions); + setResult(JSON.stringify(ShareResponse, null, 2)); + } catch (error) { + console.log('Error =>', error); + setResult('error: '.concat(getErrorString(error))); + } + }; + + const shareToTelegram = async () => { + const shareOptions = { + message: 'Example Telegram', + url: 'https://google.com', + social: Share.Social.TELEGRAM, + }; + + try { + const ShareResponse = await Share.shareSingle(shareOptions); + setResult(JSON.stringify(ShareResponse, null, 2)); + } catch (error) { + console.log('Error =>', error); + setResult('error: '.concat(getErrorString(error))); + } + }; + + const sharePdfBase64 = async () => { + const shareOptions = { + title: '', + url: pdfBase64, + }; + + try { + const ShareResponse = await Share.open(shareOptions); + setResult(JSON.stringify(ShareResponse, null, 2)); + } catch (error) { + console.log('sharePdfBase64 Error =>', error); + setResult('error: '.concat(getErrorString(error))); + } + }; + + return ( + + Welcome to React Native Share Example! + + +