├── .all-contributorsrc ├── .gitattributes ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .gitmodules ├── .npmignore ├── .screenshots ├── ios-add-framework.png └── ios-framework-searchpaths.png ├── .watchmanconfig ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── PROJECTS.md ├── README.md ├── RNSpotifyRemote.podspec ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── reactlibrary │ ├── Convert.java │ ├── RNSpotifyRemoteAppModule.java │ ├── RNSpotifyRemoteAuthModule.java │ └── RNSpotifyRemotePackage.java ├── dependabot.yml ├── docs ├── assets │ ├── css │ │ └── main.css │ ├── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png │ └── js │ │ ├── main.js │ │ └── search.js ├── enums │ ├── apiscope.html │ └── repeatmode.html ├── index.html ├── interfaces │ ├── album.html │ ├── apiconfig.html │ ├── artist.html │ ├── contentitem.html │ ├── crossfadestate.html │ ├── getchildrenitemsoptions.html │ ├── playbackoptions.html │ ├── playbackrestrictions.html │ ├── playercontext.html │ ├── playerstate.html │ ├── recommendedcontentoptions.html │ ├── spotifyauth.html │ ├── spotifyremoteapi.html │ ├── spotifyremoteevents.html │ ├── spotifysession.html │ └── track.html └── modules.html ├── example-server ├── .gitignore ├── README.md ├── index.js ├── package.json └── yarn.lock ├── example ├── .buckconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierrc.js ├── .watchmanconfig ├── App.tsx ├── AppContext.tsx ├── Components │ ├── Authenticate.tsx │ ├── ConnectButton.tsx │ ├── EnvVars.tsx │ ├── Miscelaneous.tsx │ ├── SpotifyContent.tsx │ ├── SpotifyContentListItem.tsx │ └── TransportControls.tsx ├── Readme.md ├── __tests__ │ └── App-test.tsx ├── android │ ├── app │ │ ├── BUCK │ │ ├── build.gradle │ │ ├── build_defs.bzl │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── ReactNativeFlipper.java │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── assets │ │ │ └── fonts │ │ │ │ ├── AntDesign.ttf │ │ │ │ ├── Entypo.ttf │ │ │ │ ├── EvilIcons.ttf │ │ │ │ ├── Feather.ttf │ │ │ │ ├── FontAwesome.ttf │ │ │ │ ├── FontAwesome5_Brands.ttf │ │ │ │ ├── FontAwesome5_Regular.ttf │ │ │ │ ├── FontAwesome5_Solid.ttf │ │ │ │ ├── Fontisto.ttf │ │ │ │ ├── Foundation.ttf │ │ │ │ ├── Ionicons.ttf │ │ │ │ ├── MaterialCommunityIcons.ttf │ │ │ │ ├── MaterialIcons.ttf │ │ │ │ ├── Octicons.ttf │ │ │ │ ├── Roboto.ttf │ │ │ │ ├── Roboto_medium.ttf │ │ │ │ ├── SimpleLineIcons.ttf │ │ │ │ ├── Zocial.ttf │ │ │ │ └── rubicon-icon-font.ttf │ │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── app.json ├── babel.config.js ├── index.js ├── ios │ ├── Launch Screen.storyboard │ ├── Podfile │ ├── Podfile.lock │ ├── example-Bridging-Header.h │ ├── example.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── example.xcscheme │ ├── example.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── example │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Info.plist │ │ └── main.m ├── libs │ └── react-native-dotenv.d.ts ├── metro.config.js ├── package.json ├── patches │ └── native-base+2.13.12.patch ├── scripts │ └── examples_postinstall.js ├── styles.ts ├── tsconfig.json └── yarn.lock ├── index.js ├── ios ├── Macros.h ├── NSArrayExtensions.h ├── NSArrayExtensions.m ├── RNSpotifyItem.h ├── RNSpotifyItem.m ├── RNSpotifyRemote.h ├── RNSpotifyRemote.xcodeproj │ └── project.pbxproj ├── RNSpotifyRemote.xcworkspace │ └── contents.xcworkspacedata ├── RNSpotifyRemoteAppRemote.h ├── RNSpotifyRemoteAppRemote.m ├── RNSpotifyRemoteAuth.h ├── RNSpotifyRemoteAuth.m ├── RNSpotifyRemoteConvert.h ├── RNSpotifyRemoteConvert.m ├── RNSpotifyRemoteError.h ├── RNSpotifyRemoteError.m ├── RNSpotifyRemotePromise.h ├── RNSpotifyRemotePromise.m ├── RNSpotifyRemoteSubscriptionCallback.h └── RNSpotifyRemoteSubscriptionCallback.m ├── package.json ├── react-native.config.js ├── src ├── Album.ts ├── ApiConfig.ts ├── ApiScope.ts ├── Artist.ts ├── ContentItem.ts ├── ContentType.ts ├── CrossfadeState.ts ├── GetChildrenItemsOptions.ts ├── PlaybackOptions.ts ├── PlaybackRestrictions.ts ├── PlayerContext.ts ├── PlayerState.ts ├── RecommendedContentOptions.ts ├── RepeatMode.ts ├── SpotifyAuth.ts ├── SpotifyRemote.ts ├── SpotifySession.ts ├── Track.ts ├── TypedEventEmitter.ts └── index.ts ├── tsconfig.json ├── typedoc.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-native-spotify-remote", 3 | "projectOwner": "cjam", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "cjam", 15 | "name": "Colter McQuay", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/1000288?v=4", 17 | "profile": "https://github.com/cjam", 18 | "contributions": [ 19 | "code" 20 | ] 21 | }, 22 | { 23 | "login": "lufinkey", 24 | "name": "Luis Finke", 25 | "avatar_url": "https://avatars3.githubusercontent.com/u/7820113?v=4", 26 | "profile": "https://github.com/lufinkey", 27 | "contributions": [ 28 | "ideas" 29 | ] 30 | }, 31 | { 32 | "login": "YozhikM", 33 | "name": "Stanislav", 34 | "avatar_url": "https://avatars0.githubusercontent.com/u/27273025?v=4", 35 | "profile": "https://github.com/YozhikM", 36 | "contributions": [ 37 | "code" 38 | ] 39 | }, 40 | { 41 | "login": "djwillcaine", 42 | "name": "Will Caine", 43 | "avatar_url": "https://avatars3.githubusercontent.com/u/5376687?v=4", 44 | "profile": "https://djwillcaine.com", 45 | "contributions": [ 46 | "code" 47 | ] 48 | }, 49 | { 50 | "login": "juniorklawa", 51 | "name": "Everaldo Rosa de Souza Junior", 52 | "avatar_url": "https://avatars0.githubusercontent.com/u/7197169?v=4", 53 | "profile": "http://www.estuderevisapp.com/", 54 | "contributions": [ 55 | "code" 56 | ] 57 | }, 58 | { 59 | "login": "IbrahimCanKALYA", 60 | "name": "İbrahim Can KALYA", 61 | "avatar_url": "https://avatars.githubusercontent.com/u/33005883?v=4", 62 | "profile": "http://www.cankalya.com", 63 | "contributions": [ 64 | "code" 65 | ] 66 | }, 67 | { 68 | "login": "uptotec", 69 | "name": "Mahmoud Ashraf Mahmoud", 70 | "avatar_url": "https://avatars.githubusercontent.com/u/38630967?v=4", 71 | "profile": "https://github.com/uptotec", 72 | "contributions": [ 73 | "code" 74 | ] 75 | }, 76 | { 77 | "login": "srfaytkn", 78 | "name": "Şerafettin Aytekin", 79 | "avatar_url": "https://avatars.githubusercontent.com/u/19591219?v=4", 80 | "profile": "https://linkedin.com/in/serafettinaytekin", 81 | "contributions": [ 82 | "code" 83 | ] 84 | }, 85 | { 86 | "login": "reteps", 87 | "name": "Peter Stenger", 88 | "avatar_url": "https://avatars.githubusercontent.com/u/13869303?v=4", 89 | "profile": "http://reteps.github.io", 90 | "contributions": [ 91 | "code" 92 | ] 93 | }, 94 | { 95 | "login": "reinhardholl", 96 | "name": "Reinhard Höll", 97 | "avatar_url": "https://avatars.githubusercontent.com/u/4051986?v=4", 98 | "profile": "https://github.com/reinhardholl", 99 | "contributions": [ 100 | "bug", 101 | "code" 102 | ] 103 | }, 104 | { 105 | "login": "gustavoggs", 106 | "name": "Gustavo Graña", 107 | "avatar_url": "https://avatars.githubusercontent.com/u/793491?v=4", 108 | "profile": "https://github.com/gustavoggs", 109 | "contributions": [ 110 | "bug", 111 | "code" 112 | ] 113 | }, 114 | { 115 | "login": "dylancom", 116 | "name": "Dylan", 117 | "avatar_url": "https://avatars.githubusercontent.com/u/12894112?v=4", 118 | "profile": "https://www.companjenapps.com", 119 | "contributions": [ 120 | "code" 121 | ] 122 | }, 123 | { 124 | "login": "hoangvvo", 125 | "name": "Hoang", 126 | "avatar_url": "https://avatars.githubusercontent.com/u/40987398?v=4", 127 | "profile": "https://github.com/hoangvvo", 128 | "contributions": [ 129 | "code" 130 | ] 131 | }, 132 | { 133 | "login": "pretorh", 134 | "name": "Hendri Pretorius", 135 | "avatar_url": "https://avatars.githubusercontent.com/u/4050990?v=4", 136 | "profile": "https://github.com/pretorh", 137 | "contributions": [ 138 | "bug", 139 | "code" 140 | ] 141 | }, 142 | { 143 | "login": "suryababus", 144 | "name": "suryababus", 145 | "avatar_url": "https://avatars.githubusercontent.com/u/53492132?v=4", 146 | "profile": "https://github.com/suryababus", 147 | "contributions": [ 148 | "code" 149 | ] 150 | } 151 | ], 152 | "contributorsPerLine": 7 153 | } 154 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # . 2 | 3 | Changes introduced in this pull request: 4 | 5 | - 6 | - 7 | - 8 | - 9 | 10 | ## Tasks 11 | - [ ] Add yourself as contributor via `yarn contribute` 12 | - [ ] Update `Unreleased` section of `CHANGELOG.md` with changes ([Convention](https://keepachangelog.com/en/1.0.0/)) 13 | 14 | 15 | @cjam 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # docs/ 3 | 4 | # OSX 5 | # 6 | .DS_Store 7 | .goutputstream* 8 | 9 | # node.js 10 | # 11 | node_modules/ 12 | npm-debug.log 13 | yarn-error.log 14 | 15 | 16 | dist/ 17 | 18 | # Xcode 19 | # 20 | build/ 21 | *.pbxuser 22 | !default.pbxuser 23 | *.mode1v3 24 | !default.mode1v3 25 | *.mode2v3 26 | !default.mode2v3 27 | *.perspectivev3 28 | !default.perspectivev3 29 | xcuserdata 30 | *.xccheckout 31 | *.moved-aside 32 | DerivedData 33 | *.hmap 34 | *.ipa 35 | *.xcuserstate 36 | project.xcworkspace 37 | 38 | 39 | # Android/IntelliJ 40 | # 41 | android/.project 42 | android/.settings 43 | /android/gradle/ 44 | gradlew 45 | gradlew.bat 46 | build/ 47 | .idea 48 | .gradle 49 | local.properties 50 | *.iml 51 | example/android/.project 52 | example/android/.settings 53 | *.aab 54 | 55 | # BUCK 56 | buck-out/ 57 | \.buckd/ 58 | *.keystore 59 | 60 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ios/external/SpotifySDK"] 2 | path = ios/external/SpotifySDK 3 | url = https://github.com/spotify/ios-sdk.git 4 | ignore = dirty 5 | [submodule "android/external/SpotifySDK"] 6 | path = android/external/SpotifySDK 7 | url = https://github.com/spotify/android-sdk.git 8 | ignore = dirty 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /docs 2 | /example 3 | /example-server 4 | /ios/external/SpotifySDK/docs 5 | /ios/external/SpotifySDK/DemoProjects 6 | /ios/external/SpotifySDK/img 7 | /android/.gradle/ 8 | /android/build/ 9 | /android/external/SpotifySDK/app-remote-sample 10 | /android/external/SpotifySDK/auth-sample 11 | /android/external/SpotifySDK/app-remote-lib/docs 12 | /android/external/SpotifySDK/app-remote-lib/img 13 | /android/external/SpotifySDK/auth-lib/docs 14 | /android/external/SpotifySDK/*.* 15 | gradlew 16 | gradlew.bat 17 | build/ 18 | gradlew 19 | gradlew.bat 20 | .idea 21 | local.properties 22 | *.iml 23 | yarn-error.log -------------------------------------------------------------------------------- /.screenshots/ios-add-framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/.screenshots/ios-add-framework.png -------------------------------------------------------------------------------- /.screenshots/ios-framework-searchpaths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/.screenshots/ios-framework-searchpaths.png -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Gradle Change 11 | 12 | - Updated gradle to import sdks using sourceSets 13 | 14 | ## [1.0.0-1] - 2022-06-17 15 | 16 | 17 | ### Refactor 18 | 19 | - feat: use react-native NativeEventEmitter (#189) 20 | 21 | ### Fix 22 | 23 | - Update gradle to direct include aar files to fix [#194](https://github.com/cjam/react-native-spotify-remote/issues/194) 24 | 25 | 26 | ## [0.3.11-6] - 2022-06-04 27 | 28 | ### Fix 29 | 30 | - Android null check to fix [#191](https://github.com/cjam/react-native-spotify-remote/issues/191) (Thanks @pretorh) 31 | 32 | ## [0.3.11-5] - 2022-02-10 33 | 34 | ### Fix 35 | 36 | - Remove unused maven plugin (fix build error on react-native 0.67) (#177) 37 | 38 | ## [0.3.11-4] - 2022-02-10 39 | 40 | ### Update 41 | 42 | - update legacy double-quoted import for `react-native` (#175) 43 | - update `react-native-events` to `^1.0.21` (#174) 44 | 45 | ## [0.3.11-3] - 2021-12-19 46 | 47 | ### Fix 48 | 49 | - Update react-native-events (Fix [#173](https://github.com/cjam/react-native-spotify-remote/issues/173)) 50 | - Fix PlayerRestrictions key name ([#172](https://github.com/cjam/react-native-spotify-remote/issues/172)) 51 | 52 | ## [0.3.11-2] - 2021-11-11 53 | 54 | ### Documentation 55 | 56 | - Add note for Android 11 setup 57 | 58 | ### Chore 59 | 60 | - Fix build scripts, add back the postpack script 61 | - update `.npmignore` 62 | 63 | ## [0.3.11-1] - 2021-10-17 64 | 65 | ### Update 66 | 67 | - Update Spotify SDKS (again) 68 | - Update [Readme](./README) with SDK Version Badges 69 | - Update [Contributing Guide](./CONTRIBUTING) 70 | - Add Dependabot configuration 71 | 72 | ## [0.3.11-0] - 2021-10-13 73 | 74 | ### Update 75 | 76 | - Update Spotify SDKs (thanks Hoang) 77 | - Update Android SDK to [v7.0.0-appremote_v1.2.3-auth](https://github.com/spotify/android-sdk/releases/tag/v7.0.0-appremote_v1.2.3-auth) 78 | - Update iOS SDK to [v1.2.2](https://github.com/spotify/ios-sdk/releases/tag/v1.2.2) 79 | 80 | ### Fixed 81 | 82 | - Fix Does not work when targeting Android SDK v30 (React Native 0.65+) #161 (thanks Hoang) 83 | - Fix "missing SpotifyiOS.h" in example #78 #63 (thanks @dylancom) 84 | - Fix example project podfile to build in XCode 12.5 85 | 86 | ## [0.3.10] - 2021-05-15 87 | 88 | ### Fixed 89 | 90 | - More robust handling of session object on iOS [#146](https://github.com/cjam/react-native-spotify-remote/pull/146) (thanks @gustavoggs) 91 | 92 | ## [0.3.9] - 2021-05-17 93 | 94 | ### Fixed 95 | 96 | - Fix crash when returned auth code was null [#138](https://github.com/cjam/react-native-spotify-remote/issues/138) (thanks @reinhardholl) 97 | 98 | ## [0.3.8] - 2021-05-07 99 | 100 | ### Fixed 101 | 102 | - nil context title crash on iOS [#133](https://github.com/cjam/react-native-spotify-remote/pull/133) (thanks @srfaytkn!) 103 | - getPlayerState 'Map already consumed' error [#133](https://github.com/cjam/react-native-spotify-remote/pull/133) (thanks @srfaytkn!) 104 | 105 | ## [0.3.7] - 2021-04-19 106 | 107 | ### Fixed 108 | 109 | - Reference error for javascript projects [#127](https://github.com/cjam/react-native-spotify-remote/issues/127) 110 | 111 | ## [0.3.6] - 2021-04-15 112 | 113 | ### Added 114 | 115 | - `playerContextChanged` events to iOS & Android [#118](https://github.com/cjam/react-native-spotify-remote/pull/118) 116 | - CODE flow for Android Authentication [#121](https://github.com/cjam/react-native-spotify-remote/pull/121) 117 | 118 | ### Fixed 119 | 120 | - Fix iOS returning access token instead of session object when re-authing [#112](https://github.com/cjam/react-native-spotify-remote/pull/112) 121 | - Readme error in example code https://github.com/cjam/react-native-spotify-remote/pull/115 122 | 123 | ## [0.3.5] - 2021-02-27 124 | 125 | ### Fixed 126 | 127 | - Fix for [Issue #100](https://github.com/cjam/react-native-spotify-remote/issues/100) 128 | 129 | ## [0.3.4] - 2021-02-15 130 | 131 | ### Fixed 132 | 133 | - Renamed `RNSpotifyConvert` -> `RNSpotifyRemoteConvert` to fix [Issue #94](https://github.com/cjam/react-native-spotify-remote/issues/94) ([IbrahimCanKALYA](https://github.com/IbrahimCanKALYA)) 134 | - Fix [Issue #97](https://github.com/cjam/react-native-spotify-remote/issues/97) 135 | 136 | ### Updated 137 | 138 | - Allow partial item to be passed into `playItemWithIndex` ([PR #91](https://github.com/cjam/react-native-spotify-remote/pull/91)) 139 | - Example to use library from source instead of installing local version 140 | - Autogenerated documentation 141 | 142 | ## [0.3.3] - 2020-12-13 143 | 144 | ### Fixed 145 | 146 | - Updated Peer Dependency on React Native to `>=0.60` [Issue #80](https://github.com/cjam/react-native-spotify-remote/issues/80) 147 | 148 | ### Added 149 | 150 | - Better error messages on connection failures [Issue #65](https://github.com/cjam/react-native-spotify-remote/issues/65) 151 | - iOS now checks to see if Spotify is installed 152 | - License file 153 | 154 | ### Updated 155 | 156 | - Example app to RN 63.4 157 | 158 | ## [0.3.2] - 2020-05-17 159 | 160 | ### Fixed 161 | 162 | - added some defaults for ApiConfig 163 | 164 | ### Added 165 | 166 | - More documentation around setting up android project 167 | 168 | ## [0.3.1] - 2020-05-17 (pre-release) 169 | 170 | ### Fixed 171 | 172 | - added some additional exports to `index.ts` to support missing typings 173 | 174 | ## [0.3.0] - 2020-05-16 (pre-release) 175 | 176 | ### Changed 177 | 178 | - `ApiConfig.scope` to `ApiConfig.scopes` which is now of type `ApiScope[]` and also aligns to the web api scopes 179 | - `ApiScope` enum values are now same as web api instead of bit flags 180 | - `PlayerState.paused` -> `PlayerState.isPaused` to align with Spotify's iOS/Android sdk's 181 | 182 | ### Added 183 | 184 | - Android Support! Big thanks to @YozhikM for the initial work on this 185 | - `PlaybackRestrictions.canSeek` 186 | - `SpotifyRemote.getChildrenOfItem` now has optional `options:GetChildrenItemsOptions` for android paging 187 | - `SpotifyAuth.authorize` to replace `SpotifyAuth.initialize` which will now return a session object instead of just a token 188 | 189 | ### Deprecated 190 | 191 | - `SpotifyAuth.initialize` in favor of `SpotifyAuth.authorize` 192 | 193 | ## [0.2.2] - 2020-03-22 194 | 195 | ### Changed 196 | 197 | - Removed logging on release builds [Issue #31](https://github.com/cjam/react-native-spotify-remote/issues/31) 198 | 199 | ## [0.2.1] - 2020-03-22 200 | 201 | ### Fixed 202 | 203 | - Playing Playlist Item would throw exception on PlayerState update [Issue #35](https://github.com/cjam/react-native-spotify-remote/issues/35) 204 | - Safer use of the remote apis [Issue #32](https://github.com/cjam/react-native-spotify-remote/issues/32) 205 | 206 | ## [0.2.0] - 2020-02-19 207 | 208 | ### Changed 209 | 210 | - Spotify SDK from 1.2.0 to 1.2.2 211 | - Example App to use an App Context so that components could be factored to separate files 212 | 213 | ### Added 214 | 215 | - `ApiConfig` (Used to authenticate and initialize session with `SpotifyAuth`) 216 | - `PlayURI` - URI to play when authorizing ([Issue #29](https://github.com/cjam/react-native-spotify-remote/issues/29)) 217 | - `showDialog` - Whether or not to show the auth dialog 218 | - `SpotifyAuth` 219 | - `endSession()` - Ends current session 220 | - `getSession()` - Gets the current session object 221 | - `SpotifySession` - Session Object Definition 222 | - `SpotifyRemote` 223 | - `disconnect()` - Disconnects the Remote from Spotify 224 | - [Feature Matrix](./README.md#Features) to Readme (Docs) 225 | - Example of queuing many tracks 226 | - Requirement of XCode 11 227 | 228 | ## [0.1.1] - 2020-01-21 229 | 230 | ### Fixed 231 | 232 | - Missing SpotifyiOS headers / Framework [Issue #25](https://github.com/cjam/react-native-spotify-remote/issues/25) 233 | 234 | ## [0.1.0] - 2020-01-17 235 | 236 | ### Changed 237 | 238 | - `getRecommendedContentItems` now takes `options` object instead of `ContentType` 239 | - Example app to more fully exercise exposed functionality [Issue #20](https://github.com/cjam/react-native-spotify-remote/issues/20) 240 | 241 | ### Fixed 242 | 243 | - playerStateChanged event not triggered [Issue #14](https://github.com/cjam/react-native-spotify-remote/issues/14) 244 | 245 | ### Added 246 | 247 | - `playItem` 248 | - `playItemWithIndex` for [Issue #15](https://github.com/cjam/react-native-spotify-remote/issues/15) 249 | - `getRootContentItems` 250 | - `getContentItemForUri` 251 | - `getCrossfadeState` 252 | - `Track` Properties 253 | - `saved` 254 | - `episode` 255 | - `podcast` 256 | - `ContentItem` Properties 257 | - `availableOffline` 258 | - `children` 259 | 260 | ## [0.0.8] - 2019-12-14 261 | 262 | ### Fixed 263 | 264 | - #12: 'React/RCTConvert.h' file not found 265 | 266 | ### Added 267 | 268 | - Troubleshooting section to readme 269 | 270 | ## [0.0.7] - 2019-12-13 271 | 272 | ### Fixed 273 | 274 | - Error in Cocoapod install docs 275 | 276 | ## [0.0.6] - 2019-12-13 277 | 278 | ### Added 279 | 280 | - Cocoapod support 281 | - RN >= 0.60 support 282 | 283 | ## [0.0.5] - 2019-03-16 284 | 285 | ### Fixed 286 | 287 | - Usage in README as it did not work 288 | 289 | ## [0.0.4] - 2019-03-16 290 | 291 | ### Added 292 | 293 | - Example Server 294 | - Example Project 295 | 296 | ## [0.0.2] - 2019-03-13 297 | 298 | ### Added 299 | 300 | - Surfacing errors on iOS Authentication flow 301 | 302 | ### Changed 303 | 304 | - Updates to API Docs 305 | 306 | ## [0.0.1] - 2019-03-13 307 | 308 | ### Added 309 | 310 | - iOS Auth Support 311 | - iOS App Remote 312 | - Minor API Documentation 313 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome in any form. Fork the repo and issue a PR. It's a pretty lean team ([me](https://github.com/cjam)) at the moment so I'm focusing on code over documentation. 4 | 5 | The following is a guide mostly for me so next time I come back to this project I know how to work with it. 6 | 7 | ### Development 8 | 9 | Developing this library is most easily done with the [Example Project](./example). Since we need Spotify on the device to verify functionality, the recommended approach is to add UI to the main screen that exercises the API, making functionality easy to verify and debug. 10 | 11 | In order for the example app to function correctly, there are a few things that need to be setup: 12 | 13 | #### Install Dependencies 14 | 15 | You must run `yarn` in: 16 | 17 | - repo root 18 | - `example/` 19 | - `example-server/` 20 | 21 | You must run `pod install` in `example/ios/` 22 | 23 | #### Example Server Environment 24 | 25 | - Need a `.env` file in the root of this folder with the following contents: 26 | 27 | ```env 28 | SPOTIFY_CLIENT_ID="" 29 | SPOTIFY_REDIRECT_URL="example://spotify-login-callback" 30 | SPOTIFY_TOKEN_REFRESH_URL="http:///refresh" 31 | SPOTIFY_TOKEN_SWAP_URL="http://:3001/swap" 32 | ``` 33 | 34 | #### Run Example 35 | 36 | From repo root run `yarn example` 37 | 38 | #### Updating `react-native-spotify-remote` in Example 39 | 40 | Changes in the `react-native-spotify-remote` package (i.e. modifying the api or Typescript) should be automatically reloaded within the example app. 41 | 42 | ### Updating External Spotify SDK's 43 | 44 | 1. Update SDKS 45 | #### Update Both SDK's to latest 46 | ```sh 47 | git submodule update --remote --merge 48 | ``` 49 | **OR** 50 | #### Update One SDK to specific commit 51 | ```sh 52 | cd ./ios/external/SpotifySDK/ && git checkout v 53 | ``` 54 | 2. Update badges in [Readme](./README) 55 | 56 | - Update the version in the Badge 57 | - Update the links to the correct commit 58 | 59 | 3. Commit changes 60 | 61 | ### Publishing 62 | 63 | Unfortunately, this package doesn't yet have Continuous Deployment setup but it does have an automated release script. 64 | 65 | To release a new version of the package: 66 | 67 | - Make sure the _Unreleased_ section in [Changelog](./CHANGELOG) has been updated with changes 68 | - Update any contributors (see below for adding contributors) 69 | - Make sure all changes have been committed 70 | - run `npm run release` and follow instructions 71 | 72 | ### Adding Contributors 73 | 74 | This repo uses the [all contributors cli](https://allcontributors.org/docs/en/cli/usage) to maintain contributor lists in the readme. 75 | 76 | - Finding missing ones can be done via 77 | 78 | `yarn all-contributors check` (isn't super reliable) 79 | 80 | - Add contributor 81 | `yarn all-contributors add` 82 | 83 | - Generate Readme 84 | `yarn all-contributors generate` 85 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Colter McQuay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /PROJECTS.md: -------------------------------------------------------------------------------- 1 | ## Projects using this library 2 | 3 | The following projects are using this library to do great things: 4 | 5 | - https://github.com/hardcore-study-group/music-shorts -------------------------------------------------------------------------------- /RNSpotifyRemote.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = "RNSpotifyRemote" 7 | s.version = package['version'] 8 | s.summary = package['description'] 9 | s.license = package['license'] 10 | 11 | s.authors = package['author'] 12 | s.homepage = package['repository']['url'] 13 | s.platform = :ios, "9.0" 14 | 15 | s.source = { :git => package['repository']['url'], :tag => "v#{s.version}", :submodules => true } 16 | s.source_files = "ios/*.{h,m}","ios/external/SpotifySDK/SpotifyiOS.framework/**/Headers/*.{h,m}" 17 | s.preserve_path = "ios/external/SpotifySDK/SpotifyiOS.framework" 18 | s.vendored_frameworks = "ios/external/SpotifySDK/SpotifyiOS.framework" 19 | 20 | s.dependency 'React-Core' 21 | 22 | end -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | def DEFAULT_COMPILE_SDK_VERSION = 29 2 | def DEFAULT_MIN_SDK_VERSION = 16 3 | def DEFAULT_TARGET_SDK_VERSION = 29 4 | 5 | def safeExtGet(prop, fallback) { 6 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 7 | } 8 | 9 | apply plugin: 'com.android.library' 10 | 11 | buildscript { 12 | // The Android Gradle plugin is only required when opening the android folder stand-alone. 13 | // This avoids unnecessary downloads and potential conflicts when the library is included as a 14 | // module dependency in an application project. 15 | // ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies 16 | if (project == rootProject) { 17 | repositories { 18 | google() 19 | mavenCentral() 20 | jcenter() 21 | } 22 | dependencies { 23 | classpath 'com.android.tools.build:gradle:3.4.1' 24 | } 25 | } 26 | } 27 | 28 | apply plugin: 'com.android.library' 29 | 30 | android { 31 | compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION) 32 | defaultConfig { 33 | minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION) 34 | targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION) 35 | versionCode 1 36 | versionName "1.0" 37 | } 38 | 39 | buildTypes { 40 | release { 41 | minifyEnabled false 42 | } 43 | } 44 | lintOptions { 45 | disable 'GradleCompatible' 46 | } 47 | compileOptions { 48 | sourceCompatibility JavaVersion.VERSION_1_8 49 | targetCompatibility JavaVersion.VERSION_1_8 50 | } 51 | sourceSets { 52 | main { 53 | jniLibs.srcDirs = ['external'] 54 | } 55 | } 56 | } 57 | 58 | repositories { 59 | mavenLocal() 60 | maven { 61 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 62 | url "$rootDir/../node_modules/react-native/android" 63 | } 64 | google() 65 | mavenCentral() 66 | jcenter() 67 | } 68 | 69 | dependencies { 70 | implementation (files("external/SpotifySDK/auth-lib/spotify-auth-release-1.2.3.aar")) 71 | implementation (files("external/SpotifySDK/app-remote-lib/spotify-app-remote-release-0.7.2.aar")) 72 | implementation "com.google.code.gson:gson:2.8.5" // needed by spotify-app-remote 73 | //noinspection GradleDynamicVersion 74 | implementation "com.facebook.react:react-native:+" // From node_modules 75 | } 76 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactlibrary/Convert.java: -------------------------------------------------------------------------------- 1 | package com.reactlibrary; 2 | 3 | import com.facebook.react.bridge.Arguments; 4 | import com.facebook.react.bridge.ReadableArray; 5 | import com.facebook.react.bridge.ReadableMap; 6 | import com.facebook.react.bridge.WritableArray; 7 | import com.facebook.react.bridge.WritableMap; 8 | import com.spotify.protocol.types.Album; 9 | import com.spotify.protocol.types.Artist; 10 | import com.spotify.protocol.types.CrossfadeState; 11 | import com.spotify.protocol.types.ImageUri; 12 | import com.spotify.protocol.types.ListItem; 13 | import com.spotify.protocol.types.ListItems; 14 | import com.spotify.protocol.types.PlayerOptions; 15 | import com.spotify.protocol.types.PlayerRestrictions; 16 | import com.spotify.protocol.types.PlayerState; 17 | import com.spotify.protocol.types.PlayerContext; 18 | import com.spotify.protocol.types.Track; 19 | import com.spotify.sdk.android.auth.AuthorizationResponse; 20 | 21 | import java.util.Calendar; 22 | 23 | public class Convert { 24 | 25 | public static ReadableMap toMap(AuthorizationResponse response){ 26 | if(response != null) { 27 | WritableMap map = Arguments.createMap(); 28 | Calendar expirationDate = Calendar.getInstance(); 29 | expirationDate.add(Calendar.SECOND,response.getExpiresIn()); 30 | 31 | switch (response.getType()) { 32 | case TOKEN: 33 | map.putString("accessToken", response.getAccessToken()); 34 | break; 35 | 36 | case CODE: 37 | map.putString("accessToken", response.getCode()); 38 | break; 39 | } 40 | map.putString("expirationDate", expirationDate.toString()); 41 | map.putBoolean("expired",Calendar.getInstance().after(expirationDate)); 42 | return map; 43 | }else { 44 | return null; 45 | } 46 | } 47 | 48 | 49 | public static ReadableArray toArray(ListItems listItems) { 50 | WritableArray array = Arguments.createArray(); 51 | for (ListItem item : listItems.items) { 52 | array.pushMap(Convert.toMap(item)); 53 | } 54 | return array; 55 | } 56 | 57 | public static ReadableMap toMap(ListItem item) { 58 | WritableMap map = Arguments.createMap(); 59 | map.putString("title", item.title); 60 | map.putString("subtitle", item.subtitle); 61 | map.putString("id", item.id); 62 | map.putString("uri", item.uri); 63 | map.putBoolean("playable", item.playable); 64 | 65 | // Not supported by android SDK, so put empty to maintain signature 66 | map.putArray("children", Arguments.createArray()); 67 | map.putBoolean("container", item.hasChildren); 68 | map.putBoolean("availableOffline", false); 69 | 70 | return map; 71 | } 72 | 73 | public static ListItem toItem(ReadableMap map){ 74 | ListItem item = new ListItem( 75 | map.getString("id"), 76 | map.getString("uri"), 77 | new ImageUri(""), 78 | map.getString("title"), 79 | map.getString("subtitle"), 80 | map.getBoolean("playable"), 81 | map.getBoolean("container") 82 | ); 83 | return item; 84 | } 85 | 86 | public static ReadableMap toMap(CrossfadeState state) { 87 | WritableMap map = Arguments.createMap(); 88 | map.putBoolean("enabled", state.isEnabled); 89 | map.putInt("duration", state.duration); 90 | return map; 91 | } 92 | 93 | public static ReadableMap toMap(Album album) { 94 | WritableMap map = Arguments.createMap(); 95 | map.putString("name", album.name); 96 | map.putString("uri", album.uri); 97 | return map; 98 | } 99 | 100 | public static ReadableMap toMap(Artist artist) { 101 | WritableMap map = Arguments.createMap(); 102 | map.putString("name", artist.name); 103 | map.putString("uri", artist.uri); 104 | return map; 105 | } 106 | 107 | public static ReadableMap toMap(Track track) { 108 | if (track == null) { 109 | return null; 110 | } 111 | WritableMap map = Arguments.createMap(); 112 | 113 | map.putDouble("duration", (double) track.duration); 114 | map.putBoolean("isPodcast", track.isPodcast); 115 | map.putBoolean("isEpisode", track.isEpisode); 116 | map.putString("uri", track.uri); 117 | map.putString("name", track.name); 118 | map.putMap("artist", Convert.toMap(track.artist)); 119 | map.putMap("album", Convert.toMap(track.album)); 120 | 121 | return map; 122 | } 123 | 124 | public static ReadableMap toMap(PlayerOptions options) { 125 | WritableMap map = Arguments.createMap(); 126 | map.putDouble("repeatMode", options.repeatMode); 127 | map.putBoolean("isShuffling", options.isShuffling); 128 | return map; 129 | } 130 | 131 | public static ReadableMap toMap(PlayerRestrictions restrictions) { 132 | WritableMap map = Arguments.createMap(); 133 | 134 | map.putBoolean("canRepeatContext", restrictions.canRepeatContext); 135 | map.putBoolean("canRepeatTrack", restrictions.canRepeatTrack); 136 | map.putBoolean("canSeek", restrictions.canSeek); 137 | map.putBoolean("canSkipNext", restrictions.canSkipNext); 138 | map.putBoolean("canSkipPrevious", restrictions.canSkipPrev); 139 | map.putBoolean("canToggleShuffle", restrictions.canToggleShuffle); 140 | 141 | return map; 142 | } 143 | 144 | 145 | public static WritableMap toMap(PlayerState playerState) { 146 | WritableMap map = Arguments.createMap(); 147 | 148 | map.putBoolean("isPaused", playerState.isPaused); 149 | map.putDouble("playbackPosition", (double) playerState.playbackPosition); 150 | map.putDouble("playbackSpeed", playerState.playbackSpeed); 151 | map.putMap("playbackOptions", Convert.toMap(playerState.playbackOptions)); 152 | map.putMap("playbackRestrictions", Convert.toMap(playerState.playbackRestrictions)); 153 | map.putMap("track", Convert.toMap(playerState.track)); 154 | 155 | return map; 156 | } 157 | 158 | 159 | public static ReadableMap toMap(PlayerContext playerContext) { 160 | WritableMap map = Arguments.createMap(); 161 | 162 | map.putString("title", playerContext.title); 163 | map.putString("uri", playerContext.uri); 164 | 165 | return map; 166 | } 167 | 168 | 169 | } 170 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactlibrary/RNSpotifyRemoteAuthModule.java: -------------------------------------------------------------------------------- 1 | 2 | package com.reactlibrary; 3 | 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | 7 | import com.facebook.react.bridge.ActivityEventListener; 8 | import com.facebook.react.bridge.Promise; 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 11 | import com.facebook.react.bridge.ReactMethod; 12 | import com.facebook.react.bridge.ReadableArray; 13 | import com.facebook.react.bridge.ReadableMap; 14 | import com.facebook.react.module.annotations.ReactModule; 15 | import com.spotify.android.appremote.api.ConnectionParams; 16 | import com.spotify.sdk.android.auth.AuthorizationClient; 17 | import com.spotify.sdk.android.auth.AuthorizationRequest; 18 | import com.spotify.sdk.android.auth.AuthorizationResponse; 19 | 20 | import java.util.ArrayList; 21 | 22 | 23 | @ReactModule(name = "RNSpotifyRemoteAuth") 24 | public class RNSpotifyRemoteAuthModule extends ReactContextBaseJavaModule implements ActivityEventListener { 25 | 26 | private static final int REQUEST_CODE = 1337; 27 | private final ReactApplicationContext reactContext; 28 | private Promise authPromise; 29 | private AuthorizationResponse mAuthResponse; 30 | private ReadableMap mConfig; 31 | private ConnectionParams.Builder mConnectionParamsBuilder; 32 | 33 | 34 | public ConnectionParams.Builder getConnectionParamsBuilder() { 35 | return mConnectionParamsBuilder; 36 | } 37 | 38 | public RNSpotifyRemoteAuthModule(ReactApplicationContext reactContext) { 39 | super(reactContext); 40 | reactContext.addActivityEventListener(this); 41 | this.reactContext = reactContext; 42 | } 43 | 44 | @ReactMethod 45 | public void authorize(ReadableMap config, Promise promise) { 46 | mConfig = config; 47 | String clientId = mConfig.getString("clientID"); 48 | String redirectUri = mConfig.getString("redirectURL"); 49 | Boolean showDialog = mConfig.getBoolean("showDialog"); 50 | String[] scopes = convertScopes(mConfig); 51 | AuthorizationResponse.Type responseType = mConfig.hasKey("authType") ? 52 | AuthorizationResponse.Type.valueOf(mConfig.getString("authType")) 53 | : AuthorizationResponse.Type.TOKEN; 54 | 55 | mConnectionParamsBuilder = new ConnectionParams.Builder(clientId) 56 | .setRedirectUri(redirectUri) 57 | .showAuthView(showDialog); 58 | 59 | authPromise = promise; 60 | 61 | AuthorizationRequest.Builder builder = new AuthorizationRequest.Builder( 62 | clientId, 63 | responseType, 64 | redirectUri 65 | ); 66 | builder.setScopes(scopes); 67 | AuthorizationRequest request = builder.build(); 68 | AuthorizationClient.openLoginActivity(getCurrentActivity(), REQUEST_CODE, request); 69 | } 70 | 71 | @Override 72 | public void onNewIntent(Intent intent) { 73 | } 74 | 75 | @Override 76 | public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { 77 | if (requestCode == REQUEST_CODE) { 78 | AuthorizationResponse response = AuthorizationClient.getResponse(resultCode, data); 79 | 80 | switch (response.getType()) { 81 | // Response was successful and contains auth token/code 82 | case TOKEN: 83 | case CODE: 84 | if (authPromise != null) { 85 | mAuthResponse = response; 86 | authPromise.resolve(Convert.toMap(response)); 87 | } 88 | break; 89 | 90 | // Auth flow returned an error 91 | case ERROR: 92 | if (authPromise != null) { 93 | String code = response.getCode(); 94 | String error = response.getError(); 95 | authPromise.reject(code, error); 96 | mConnectionParamsBuilder = null; 97 | } 98 | break; 99 | 100 | // Most likely auth flow was cancelled 101 | default: 102 | if (authPromise != null) { 103 | String code = "500"; 104 | String error = "Cancelled"; 105 | authPromise.reject(code, error); 106 | mConnectionParamsBuilder = null; 107 | } 108 | } 109 | } 110 | } 111 | 112 | @ReactMethod 113 | public void getSession(Promise promise) { 114 | promise.resolve(Convert.toMap(mAuthResponse)); 115 | } 116 | 117 | @ReactMethod 118 | public void endSession(Promise promise) { 119 | mAuthResponse = null; 120 | mConnectionParamsBuilder = null; 121 | mConfig = null; 122 | 123 | AuthorizationClient.clearCookies(this.getReactApplicationContext()); 124 | 125 | RNSpotifyRemoteAppModule remoteModule = reactContext.getNativeModule(RNSpotifyRemoteAppModule.class); 126 | if (remoteModule != null) { 127 | remoteModule.disconnect(promise); 128 | } else { 129 | promise.resolve(null); 130 | } 131 | } 132 | 133 | public String[] convertScopes(ReadableMap config) { 134 | ReadableArray arrayOfScopes = config.getArray("scopes"); 135 | 136 | ArrayList scopesArrayList = new ArrayList(); 137 | 138 | for (int i = 0; i < arrayOfScopes.size(); i++) { 139 | String scope = arrayOfScopes.getString(i); 140 | scopesArrayList.add(scope); 141 | } 142 | 143 | return scopesArrayList.toArray(new String[scopesArrayList.size()]); 144 | } 145 | 146 | @Override 147 | public String getName() { 148 | return "RNSpotifyRemoteAuth"; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactlibrary/RNSpotifyRemotePackage.java: -------------------------------------------------------------------------------- 1 | 2 | package com.reactlibrary; 3 | 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import com.facebook.react.ReactPackage; 9 | import com.facebook.react.bridge.NativeModule; 10 | import com.facebook.react.bridge.ReactApplicationContext; 11 | import com.facebook.react.uimanager.ViewManager; 12 | import com.facebook.react.bridge.JavaScriptModule; 13 | public class RNSpotifyRemotePackage implements ReactPackage { 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Arrays.asList(new RNSpotifyRemoteAuthModule(reactContext), new RNSpotifyRemoteAppModule(reactContext)); 17 | } 18 | 19 | // Deprecated from RN 0.47 20 | public List> createJSModules() { 21 | return Collections.emptyList(); 22 | } 23 | 24 | @Override 25 | public List createViewManagers(ReactApplicationContext reactContext) { 26 | return Collections.emptyList(); 27 | } 28 | } -------------------------------------------------------------------------------- /dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for npm 5 | - package-ecosystem: "npm" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /example-server/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # expo 4 | .expo/ 5 | 6 | # dependencies 7 | node_modules 8 | 9 | # misc 10 | .env 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | -------------------------------------------------------------------------------- /example-server/README.md: -------------------------------------------------------------------------------- 1 | # Example Token Refresh Server 2 | 3 | An example server capable of swapping and refreshing tokens provided by Spotify API. 4 | 5 | ## Usage 6 | 7 | 1. Install dependencies using: 8 | ```sh 9 | yarn # or npm install 10 | ``` 11 | 2. Create a `.env` file in the root of this directory with the following entries acquired from [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/applications) : 12 | > ⚠️ Don't commit the `.env` file to your repo ⚠️ 13 | 14 | ```env 15 | SPOTIFY_CLIENT_ID="client_id_from_spotify_dashboard" 16 | SPOTIFY_CLIENT_SECRET="client_secret_from_spotify_dashboard" 17 | SPOTIFY_CLIENT_CALLBACK="callback_registered_in_spotify_dashboard" 18 | ENCRYPTION_SECRET="THISWILLBEABIGSECRET" 19 | ENCRYPTION_METHOD="aes-256-ctr" 20 | ``` 21 | Can also specify `PORT` if you want to run it on something other than 3000. 22 | > Optionally this can be done on the command line as well when starting up the server via node 23 | 24 | 3. Run server using: `yarn start` 25 | 4. In your react-native app set `tokenSwapURL` to `http://:/swap` and `tokenRefreshURL` to `http://:/refresh`, replacing `` and `` with your server URL and port. 26 | -------------------------------------------------------------------------------- /example-server/index.js: -------------------------------------------------------------------------------- 1 | // npm deps 2 | const express = require('express'); 3 | const https = require('https'); 4 | const crypto = require('crypto'); 5 | const { URL } = require('url'); 6 | const QueryString = require('querystring'); 7 | const dotenv = require('dotenv'); 8 | dotenv.config(); 9 | 10 | // Require the framework and instantiate it 11 | const app = express(); 12 | 13 | // init spotify config 14 | const spClientId = process.env.SPOTIFY_CLIENT_ID; 15 | const spClientSecret = process.env.SPOTIFY_CLIENT_SECRET; 16 | const spClientCallback = process.env.SPOTIFY_CLIENT_CALLBACK; 17 | const authString = Buffer.from(spClientId+':'+spClientSecret).toString('base64'); 18 | const authHeader = `Basic ${authString}`; 19 | const spotifyEndpoint = 'https://accounts.spotify.com/api/token'; 20 | 21 | // encryption 22 | const encSecret = process.env.ENCRYPTION_SECRET; 23 | const encMethod = process.env.ENCRYPTION_METHOD || "aes-256-ctr"; 24 | const encrypt = (text) => { 25 | const aes = crypto.createCipher(encMethod, encSecret); 26 | let encrypted = aes.update(text, 'utf8', 'hex'); 27 | encrypted += aes.final('hex'); 28 | return encrypted; 29 | }; 30 | const decrypt = (text) => { 31 | const aes = crypto.createDecipher(encMethod, encSecret); 32 | let decrypted = aes.update(text, 'hex', 'utf8'); 33 | decrypted += aes.final('utf8'); 34 | return decrypted; 35 | }; 36 | 37 | // handle sending POST request 38 | function postRequest(url, data={}) 39 | { 40 | return new Promise((resolve, reject) => { 41 | // build request data 42 | url = new URL(url); 43 | const reqData = { 44 | protocol: url.protocol, 45 | hostname: url.hostname, 46 | port: url.port, 47 | path: url.pathname, 48 | method: 'POST', 49 | headers: { 50 | 'Authorization': authHeader, 51 | 'Content-Type': 'application/x-www-form-urlencoded' 52 | } 53 | } 54 | 55 | // create request 56 | const req = https.request(reqData, (res) => { 57 | // build response 58 | let buffers = []; 59 | res.on('data', (chunk) => { 60 | buffers.push(chunk); 61 | }); 62 | 63 | res.on('end', () => { 64 | // parse response 65 | let result = null; 66 | try 67 | { 68 | result = Buffer.concat(buffers); 69 | result = result.toString(); 70 | var contentType = res.headers['content-type']; 71 | if(typeof contentType == 'string') 72 | { 73 | contentType = contentType.split(';')[0].trim(); 74 | } 75 | if(contentType == 'application/x-www-form-urlencoded') 76 | { 77 | result = QueryString.parse(result); 78 | } 79 | else if(contentType == 'application/json') 80 | { 81 | result = JSON.parse(result); 82 | } 83 | } 84 | catch(error) 85 | { 86 | error.response = res; 87 | error.data = result; 88 | reject(error); 89 | return; 90 | } 91 | resolve({response: res, result: result}); 92 | }) 93 | }); 94 | 95 | // handle error 96 | req.on('error', (error) => { 97 | reject(error); 98 | }); 99 | 100 | // send 101 | data = QueryString.stringify(data); 102 | req.write(data); 103 | req.end(); 104 | }); 105 | } 106 | 107 | // support form body 108 | app.use(express.urlencoded({extended: false})); 109 | 110 | /** 111 | * Swap endpoint 112 | * Uses an authentication code on body to request access and refresh tokens 113 | */ 114 | app.post('/swap', async (req, res) => { 115 | try { 116 | console.log("SWAP"); 117 | // build request data 118 | const reqData = { 119 | grant_type: 'authorization_code', 120 | redirect_uri: spClientCallback, 121 | code: req.body.code 122 | }; 123 | 124 | // get new token from Spotify API 125 | const { response, result } = await postRequest(spotifyEndpoint, reqData); 126 | 127 | // encrypt refresh_token 128 | if (result.refresh_token) { 129 | result.refresh_token = encrypt(result.refresh_token); 130 | } 131 | 132 | // send response 133 | res.status(response.statusCode).json(result); 134 | } 135 | catch(error) { 136 | if(error.response) { 137 | res.status(error.response.statusCode); 138 | } 139 | else { 140 | res.status(500); 141 | } 142 | if(error.data) { 143 | res.send(error.data); 144 | } 145 | else { 146 | res.send(""); 147 | } 148 | } 149 | }); 150 | 151 | /** 152 | * Refresh endpoint 153 | * Uses the refresh token on request body to get a new access token 154 | */ 155 | app.post('/refresh', async (req, res) => { 156 | console.log("REFRESH"); 157 | try { 158 | // ensure refresh token parameter 159 | if (!req.body.refresh_token) { 160 | res.status(400).json({error: 'Refresh token is missing from body'}); 161 | return; 162 | } 163 | 164 | // decrypt token 165 | const refreshToken = decrypt(req.body.refresh_token); 166 | // build request data 167 | const reqData = { 168 | grant_type: 'refresh_token', 169 | refresh_token: refreshToken 170 | }; 171 | // get new token from Spotify API 172 | const { response, result } = await postRequest(spotifyEndpoint, reqData); 173 | 174 | // encrypt refresh_token 175 | if (result.refresh_token) { 176 | result.refresh_token = encrypt(result.refresh_token); 177 | } 178 | 179 | // send response 180 | res.status(response.statusCode).json(result); 181 | } 182 | catch(error) { 183 | if(error.response) { 184 | res.status(error.response.statusCode); 185 | } 186 | else { 187 | res.status(500); 188 | } 189 | if(error.data) { 190 | res.send(error.data); 191 | } 192 | else { 193 | res.send(""); 194 | } 195 | } 196 | }); 197 | 198 | // start server 199 | const spServerPort = process.env.PORT ? parseInt(process.env.PORT) : 3000; 200 | app.listen(spServerPort, () => console.log('Example app listening on port '+spServerPort+'!')); 201 | -------------------------------------------------------------------------------- /example-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-server", 3 | "version": "1.0.0", 4 | "description": "Example server that supports token swap and refresh", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "dependencies": { 10 | "express": "^4.16.2" 11 | }, 12 | "devDependencies": { 13 | "dotenv": "^6.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | }; 5 | -------------------------------------------------------------------------------- /example/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | 3 | # specific for windows script files 4 | *.bat text eol=crlf -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | *.env* 2 | 3 | # OSX 4 | # 5 | .DS_Store 6 | 7 | # Xcode 8 | # 9 | build/ 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | *.xccheckout 20 | *.moved-aside 21 | DerivedData 22 | *.hmap 23 | *.ipa 24 | *.xcuserstate 25 | project.xcworkspace 26 | 27 | # Android/IntelliJ 28 | # 29 | build/ 30 | .idea 31 | .gradle 32 | local.properties 33 | *.iml 34 | 35 | # Visual Studio Code 36 | # 37 | .vscode/ 38 | 39 | # node.js 40 | # 41 | node_modules/ 42 | npm-debug.log 43 | yarn-error.log 44 | 45 | # BUCK 46 | buck-out/ 47 | \.buckd/ 48 | *.keystore 49 | 50 | # fastlane 51 | # 52 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 53 | # screenshots whenever they are needed. 54 | # For more information about the recommended setup visit: 55 | # https://docs.fastlane.tools/best-practices/source-control/ 56 | 57 | */fastlane/report.xml 58 | */fastlane/Preview.html 59 | */fastlane/screenshots 60 | 61 | # Bundle artifact 62 | *.jsbundle 63 | 64 | ios/Pods -------------------------------------------------------------------------------- /example/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | }; 7 | -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { View, SafeAreaView } from 'react-native'; 3 | import { Text, Root, Tabs, Tab } from 'native-base'; 4 | import AppContext, { AppContextProvider } from './AppContext'; 5 | 6 | import styles from './styles'; 7 | 8 | import ConnectButton from './Components/ConnectButton'; 9 | import Authenticate from './Components/Authenticate'; 10 | import TransportControls from './Components/TransportControls'; 11 | import SpotifyContent from './Components/SpotifyContent'; 12 | import Miscelaneous from './Components/Miscelaneous'; 13 | import EnvVars from './Components/EnvVars'; 14 | 15 | const AppLayout: React.SFC = () => { 16 | const { token, error, clearError } = useContext(AppContext); 17 | return ( 18 | 19 | {token ? 20 | <> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | : 35 | <> 36 | 37 | 38 | 39 | } 40 | 41 | {error && ( 42 | {error.code}: {error.message} 43 | )} 44 | 45 | ) 46 | } 47 | 48 | const App: React.SFC = () => { 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export default App; 61 | 62 | -------------------------------------------------------------------------------- /example/AppContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | auth, 4 | remote, 5 | ApiConfig, 6 | ApiScope, 7 | SpotifyRemoteApi, 8 | PlayerState, 9 | PlayerContext, 10 | RepeatMode, 11 | ContentItem, 12 | SpotifyAuth 13 | } from 'react-native-spotify-remote'; 14 | import { 15 | SPOTIFY_CLIENT_ID, 16 | SPOTIFY_REDIRECT_URL, 17 | SPOTIFY_TOKEN_REFRESH_URL, 18 | SPOTIFY_TOKEN_SWAP_URL 19 | } from 'react-native-dotenv'; 20 | 21 | interface AuthOptions { 22 | playURI?: string; 23 | showDialog?: boolean; 24 | autoConnect?: boolean; 25 | authType?: ApiConfig["authType"] 26 | } 27 | 28 | interface AppContextState { 29 | error?: Error & { code?: any }; 30 | playerState?: PlayerState; 31 | token?: string; 32 | isConnected?: boolean; 33 | } 34 | 35 | export interface AppContextProps extends AppContextState { 36 | onError: (err: Error) => void; 37 | authenticate: (options?: AuthOptions) => void; 38 | clearError: () => void; 39 | endSession: () => void; 40 | remote: SpotifyRemoteApi, 41 | auth: SpotifyAuth 42 | } 43 | 44 | const noop = () => { }; 45 | const DefaultContext: AppContextProps = { 46 | onError: noop, 47 | authenticate: noop, 48 | clearError: noop, 49 | endSession: noop, 50 | remote, 51 | auth 52 | } 53 | 54 | const AppContext = React.createContext(DefaultContext); 55 | 56 | class AppContextProvider extends React.Component<{}, AppContextState> { 57 | state = { 58 | isConnected: false 59 | } 60 | 61 | constructor(props: any) { 62 | super(props); 63 | this.onError = this.onError.bind(this); 64 | this.authenticate = this.authenticate.bind(this); 65 | this.clearError = this.clearError.bind(this); 66 | this.onConnected = this.onConnected.bind(this); 67 | this.onDisconnected = this.onDisconnected.bind(this); 68 | this.onPlayerStateChanged = this.onPlayerStateChanged.bind(this); 69 | this.onPlayerContextChanged = this.onPlayerContextChanged.bind(this); 70 | this.endSession = this.endSession.bind(this); 71 | } 72 | 73 | componentDidMount() { 74 | remote.on("remoteConnected", this.onConnected) 75 | .on("remoteDisconnected", this.onDisconnected) 76 | .on("playerStateChanged", this.onPlayerStateChanged) 77 | .on("playerContextChanged", this.onPlayerContextChanged); 78 | 79 | auth.getSession().then((session) => { 80 | if (session != undefined && session.accessToken != undefined) { 81 | this.setState((state) => ({ ...state, token: session.accessToken })) 82 | remote.connect(session.accessToken) 83 | .then(() => this.setState((state) => ({ 84 | ...state, 85 | isConnected: true 86 | }))) 87 | .catch(this.onError); 88 | } 89 | }); 90 | } 91 | 92 | componentWillUnmount() { 93 | remote.removeAllListeners(); 94 | } 95 | 96 | private onError(error: Error) { 97 | this.setState((state) => ({ ...state, error })) 98 | } 99 | 100 | private clearError() { 101 | this.setState((state) => ({ ...state, error: undefined })); 102 | } 103 | 104 | private onConnected() { 105 | this.setState((state) => ({ 106 | ...state, 107 | isConnected: true 108 | })); 109 | } 110 | 111 | private onDisconnected() { 112 | this.setState((state) => ({ 113 | ...state, 114 | isConnected: false 115 | })); 116 | } 117 | 118 | private onPlayerStateChanged(playerState: PlayerState) { 119 | this.setState((state) => ({ 120 | ...state, 121 | playerState 122 | })); 123 | }; 124 | 125 | private onPlayerContextChanged(playerContext: PlayerContext) { 126 | this.setState((state) => ({ 127 | ...state, 128 | playerContext 129 | })); 130 | }; 131 | 132 | private endSession() { 133 | auth.endSession().then(() => { 134 | remote.disconnect().then(() => { 135 | this.setState({ isConnected: false, token: undefined }); 136 | }); 137 | }); 138 | } 139 | 140 | private async authenticate({ playURI, showDialog = false, authType }: AuthOptions = {}) { 141 | const config: ApiConfig = { 142 | clientID: SPOTIFY_CLIENT_ID, 143 | redirectURL: SPOTIFY_REDIRECT_URL, 144 | tokenRefreshURL: SPOTIFY_TOKEN_REFRESH_URL, 145 | tokenSwapURL: SPOTIFY_TOKEN_SWAP_URL, 146 | scopes: [ApiScope.AppRemoteControlScope], 147 | playURI, 148 | showDialog, 149 | authType 150 | }; 151 | 152 | try { 153 | // Go and check if things are connected 154 | const isConnected = await remote.isConnectedAsync() 155 | this.setState((state) => ({ 156 | ...state, 157 | isConnected 158 | })); 159 | 160 | // Initialize the session 161 | const { accessToken: token } = await auth.authorize(config); 162 | this.setState((state) => ({ 163 | ...state, 164 | token 165 | })); 166 | await remote.connect(token); 167 | } catch (err) { 168 | this.onError(err); 169 | } 170 | } 171 | 172 | render() { 173 | const { children } = this.props 174 | return ( 175 | 185 | {children} 186 | 187 | ) 188 | } 189 | } 190 | 191 | export default AppContext; 192 | export { AppContextProvider }; -------------------------------------------------------------------------------- /example/Components/Authenticate.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useCallback } from 'react'; 2 | import { View, Button, Text, Toast, Switch } from 'native-base'; 3 | import styles from '../styles'; 4 | import AppContext from '../AppContext'; 5 | import { Platform } from 'react-native'; 6 | import { ApiConfig } from 'react-native-spotify-remote'; 7 | 8 | const Authenticate: React.SFC = () => { 9 | const { isConnected, token, onError, remote, authenticate } = useContext(AppContext) 10 | const [showDialog, setShowDialog] = useState(false); 11 | const [autoConnect, setAutoConnect] = useState(true); 12 | 13 | const handleConnect = useCallback((playURI?: string, authType?: ApiConfig["authType"]) => { 14 | authenticate({ 15 | showDialog, 16 | playURI, 17 | authType 18 | }); 19 | }, [showDialog, token]) 20 | return ( 21 | 22 | 23 | Connect To Spotify 24 | 25 | 26 | Show Auth Dialog: 27 | 28 | 29 | 32 | 35 | 38 | {Platform.OS === "android" && ( 39 | 42 | )} 43 | 44 | ) 45 | } 46 | 47 | export default Authenticate; -------------------------------------------------------------------------------- /example/Components/ConnectButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useCallback } from 'react'; 2 | import { View, Button, Text, Toast } from 'native-base'; 3 | import styles from '../styles'; 4 | import AppContext from '../AppContext'; 5 | import { Alert } from 'react-native'; 6 | 7 | const ConnectButton: React.SFC = () => { 8 | const { isConnected, endSession, token, onError, remote } = useContext(AppContext) 9 | 10 | const handleClick = useCallback(() => { 11 | if (!isConnected && token != undefined) { 12 | remote.connect(token).catch((e) => { 13 | // Usually if we can't connect, its because 14 | // spotify has been backgrounded and we need to 15 | // reauth to bring it back 16 | endSession(); 17 | }); 18 | } else { 19 | remote.isConnectedAsync().then((connected) => { 20 | Toast.show({ 21 | text: connected ? "Connected!" : "Not Connected", 22 | type: connected ? "success" : "danger", 23 | position: "top" 24 | }); 25 | }).catch(onError) 26 | } 27 | }, [isConnected, token]) 28 | 29 | const handleLongPress = useCallback(()=>{ 30 | Alert.alert( 31 | "Disonnect from Spotify?", 32 | "Are you sure you want to disconnect?", 33 | [ 34 | { 35 | style:"cancel", 36 | text:"Cancel", 37 | }, 38 | { 39 | style:"destructive", 40 | text:"Disconnect", 41 | onPress:()=>{ 42 | remote.disconnect().then(()=>{ 43 | Toast.show({ 44 | text:"Disconnected!", 45 | type:"warning", 46 | position:"top" 47 | }) 48 | }) 49 | } 50 | } 51 | ], 52 | ) 53 | },[]); 54 | 55 | return ( 56 | 65 | ) 66 | } 67 | 68 | export default ConnectButton; -------------------------------------------------------------------------------- /example/Components/EnvVars.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, Button, Text } from 'native-base'; 3 | import { SPOTIFY_REDIRECT_URL, SPOTIFY_TOKEN_REFRESH_URL, SPOTIFY_TOKEN_SWAP_URL } from 'react-native-dotenv'; 4 | import styles from '../styles'; 5 | 6 | export default function EnvVars () { 7 | const [show, setShow] = useState(false); 8 | return ( 9 | 10 | 13 | {show && ( 14 | 15 | App .env Variables 16 | Auth Redirect Url: {SPOTIFY_REDIRECT_URL} 17 | Token Refresh URL: {SPOTIFY_TOKEN_REFRESH_URL} 18 | Token Swap Url: {SPOTIFY_TOKEN_SWAP_URL} 19 | 20 | )} 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /example/Components/Miscelaneous.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useContext } from "react"; 2 | import { ContentItem } from "react-native-spotify-remote"; 3 | import { Alert, ScrollView } from "react-native"; 4 | import { View, Button, Text } from "native-base"; 5 | import EnvVars from "./EnvVars"; 6 | import AppContext from "../AppContext"; 7 | 8 | const Miscelaneous: React.SFC = () => { 9 | const { onError, auth, remote, endSession } = useContext(AppContext); 10 | const [item, setItem] = useState(); 11 | 12 | const getContentItemForUri = useCallback(async () => { 13 | const retrievedItem = await remote.getContentItemForUri("spotify:playlist:37i9dQZF1DWXLeA8Omikj7"); 14 | setItem(retrievedItem); 15 | }, []); 16 | 17 | const playItem = useCallback(async () => { 18 | if (item != undefined) { 19 | await remote.playItem(item); 20 | } 21 | }, [item]) 22 | 23 | const playTrackInItem = useCallback(async (index: number) => { 24 | if (item != undefined) { 25 | await remote.playItemWithIndex(item, index); 26 | } 27 | }, [item]); 28 | 29 | const queueManyTracks = useCallback(async () => { 30 | const tracks = [ 31 | "spotify:track:2dlEdDEmuQsrcXaAL3Znzi", 32 | "spotify:track:7Cuk8jsPPoNYQWXK9XRFvG", 33 | "spotify:track:0ofHAoxe9vBkTCp2UQIavz", 34 | "spotify:track:7ejK5qMqXciqGgMIqj0rFr", 35 | "spotify:track:5LYJ631w9ps5h9tdvac7yP", 36 | "spotify:track:5dRQUolXAVX3BbCiIxmSsf", 37 | "spotify:track:76798uYU1DhBRVWxSo1bhY", 38 | "spotify:track:7Ar4G7Ci11gpt6sfH9Cgz5", 39 | "spotify:track:5IMtdHjJ1OtkxbGe4zfUxQ" 40 | ]; 41 | 42 | // Need to queue each track serially 43 | for (const track of tracks) { 44 | await remote.queueUri(track); 45 | } 46 | Alert.alert(`Queued ${tracks.length} tracks`); 47 | }, []); 48 | 49 | const getCrossfadeState = useCallback(async () => { 50 | try { 51 | const cfstate = await remote.getCrossfadeState(); 52 | Alert.alert("Crossfade State", ` 53 | Enabled: ${cfstate.enabled ? "Yes" : "No"} 54 | Duration: ${cfstate.duration} ms 55 | ` 56 | ) 57 | } catch (err) { 58 | onError(err); 59 | } 60 | }, []); 61 | 62 | return ( 63 | 64 | 65 | {item == undefined ? 66 | ( 67 | 70 | ) 71 | : 72 | ( 73 | 74 | title: {item.title} 75 | subtitle: {item.subtitle} 76 | uri: {item.uri} 77 | id: {item.id} 78 | 81 | 84 | 85 | ) 86 | } 87 | 90 | 93 | 96 | 97 | 98 | 99 | ) 100 | } 101 | 102 | export default Miscelaneous; -------------------------------------------------------------------------------- /example/Components/SpotifyContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect, useContext } from "react"; 2 | import { ContentItem, remote } from "react-native-spotify-remote"; 3 | import { ActionSheet, View, Button, Text } from "native-base"; 4 | import { FlatList } from "react-native"; 5 | import SpotifyContentListItem from "./SpotifyContentListItem"; 6 | import AppContext from "../AppContext"; 7 | 8 | const SpotifyContent: React.SFC = () => { 9 | const { isConnected, onError } = useContext(AppContext) 10 | const [parentItems, setParentItems] = useState([]); 11 | const [recommended, setRecommended] = useState(true); 12 | 13 | // The current parent is the last parent if there are any 14 | const currentItem = parentItems[parentItems.length - 1]; 15 | 16 | const back = useCallback(() => { 17 | if (parentItems.length === 1) { 18 | return; 19 | } else { 20 | const newParents = [...parentItems]; 21 | newParents.pop(); 22 | setParentItems(newParents); 23 | } 24 | }, [parentItems]); 25 | 26 | const pushParent = useCallback(async (nextParent: ContentItem) => { 27 | try { 28 | if (nextParent.container && nextParent.children == undefined || nextParent.children.length === 0) { 29 | const loadedChildren = await remote.getChildrenOfItem(nextParent); 30 | const newParent = { 31 | ...nextParent, 32 | children: loadedChildren 33 | } 34 | setParentItems([...parentItems, newParent]); 35 | } else { 36 | setParentItems([...parentItems, nextParent]); 37 | } 38 | } catch (err) { 39 | onError(err); 40 | } 41 | }, [parentItems]) 42 | 43 | const fetchItems = async () => { 44 | try { 45 | let retrieved: ContentItem[] = []; 46 | if (recommended) { 47 | retrieved = await remote.getRecommendedContentItems({ type: "default", flatten: false }); 48 | } else { 49 | retrieved = await remote.getRootContentItems("default"); 50 | } 51 | 52 | const rootItem: ContentItem = { 53 | title: "", 54 | availableOffline: false, 55 | container: true, 56 | id: "", 57 | playable: false, 58 | subtitle: "", 59 | uri: "", 60 | children: retrieved 61 | } 62 | setParentItems([rootItem]); 63 | } catch (err) { 64 | onError(err); 65 | } 66 | }; 67 | 68 | const showItemActions = useCallback((item: ContentItem) => { 69 | const cancelOption = { text: 'Cancel', action: () => { } }; 70 | const actionOptions = [ 71 | { text: 'Play Uri', action: () => remote.playUri(item.uri) }, 72 | { text: 'Queue Uri', action: () => remote.queueUri(item.uri) }, 73 | { text: 'Play Item', action: () => remote.playItem(item) }, 74 | ] 75 | 76 | if (item.uri.includes("spotify:playlist:") || item.uri.includes("spotify:album:")) { 77 | actionOptions.push({ 78 | text: 'Play 4th track in playlist', 79 | action: () => remote.playItemWithIndex(item, 3) 80 | }); 81 | } 82 | 83 | ActionSheet.show({ 84 | options: [ 85 | ...actionOptions, 86 | cancelOption 87 | ], 88 | cancelButtonIndex: actionOptions.findIndex(({text})=>text===cancelOption.text), 89 | title: item.title 90 | }, async (index) => { 91 | try { 92 | const chosenOption = actionOptions[index]; 93 | if(chosenOption != undefined){ 94 | await chosenOption.action(); 95 | } 96 | } catch (err) { 97 | onError(err); 98 | } 99 | 100 | }) 101 | }, []); 102 | 103 | const selectContentItem = useCallback(async (item: ContentItem) => { 104 | if (item.container || !item.uri.toLowerCase().includes(":track:")) { 105 | await pushParent(item); 106 | } else if (item.playable) { 107 | await showItemActions(item); 108 | } 109 | }, [pushParent]) 110 | 111 | useEffect(() => { 112 | if (isConnected) { 113 | fetchItems(); 114 | } 115 | }, [isConnected, recommended]); 116 | 117 | return ( 118 | 119 | {currentItem && ( 120 | 121 | 122 | 123 | 132 | {currentItem.title} 133 | 134 | 135 | 136 | { 141 | if (it.playable) { 142 | showItemActions(it); 143 | } 144 | }} 145 | item={item} 146 | />} 147 | /> 148 | 149 | 150 | 153 | 156 | 157 | 158 | )} 159 | 160 | ) 161 | } 162 | 163 | 164 | export default SpotifyContent; -------------------------------------------------------------------------------- /example/Components/SpotifyContentListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ListItem, Left, View, Text, Body, Right } from "native-base"; 3 | import { ContentItem } from "react-native-spotify-remote"; 4 | 5 | const SpotifyContentListItem: React.SFC<{ onPress?: (item: ContentItem) => void, onLongPress?: (item: ContentItem) => void, item: ContentItem }> = ({ 6 | item, 7 | onPress, 8 | onLongPress 9 | }) => { 10 | const { 11 | title, 12 | subtitle, 13 | id, 14 | playable, 15 | uri, 16 | container, 17 | availableOffline, 18 | children: itemChildren = [] 19 | } = item; 20 | 21 | return ( 22 | onPress && onPress(item)} onLongPress={() => onLongPress && onLongPress(item)}> 23 | 24 | 25 | {title} 26 | {subtitle} 27 | 28 | 29 | 30 | {availableOffline && "Offline"} 31 | 32 | 33 | {itemChildren.length} 34 | 35 | 36 | ) 37 | } 38 | 39 | export default SpotifyContentListItem; -------------------------------------------------------------------------------- /example/Components/TransportControls.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useCallback } from 'react'; 2 | import { View, Button, Text, Toast, Switch, Segment, Icon } from 'native-base'; 3 | import styles from '../styles'; 4 | import AppContext from '../AppContext'; 5 | import { RepeatMode } from 'react-native-spotify-remote'; 6 | import Slider from '@react-native-community/slider'; 7 | import { Duration } from 'luxon'; 8 | 9 | /** 10 | * Converts a duration (in ms) to a human readable string 11 | * 12 | * @export 13 | * @param {number} durationMs - Duration in Milliseconds 14 | * @returns 15 | */ 16 | export function displayDuration(durationMs: number) { 17 | const d = Duration.fromMillis(durationMs); 18 | return d.hours > 0 ? 19 | d.toFormat("h:mm:ss") 20 | : d.toFormat("m:ss"); 21 | } 22 | 23 | 24 | const TransportControls: React.SFC = () => { 25 | const { 26 | playerState: { 27 | isPaused = false, 28 | playbackOptions: { 29 | isShuffling = false, 30 | repeatMode = RepeatMode.Off 31 | } = {}, 32 | playbackPosition = 0, 33 | playbackRestrictions:{ 34 | canRepeatContext = true, 35 | canRepeatTrack = true, 36 | canSkipNext = true, 37 | canSkipPrevious = true, 38 | canToggleShuffle = true 39 | } = {}, 40 | track: { 41 | duration = 0, 42 | name = "", 43 | artist: { 44 | name: artistName = "" 45 | } = {} 46 | } = {} 47 | } = {}, 48 | onError, 49 | remote 50 | } = useContext(AppContext); 51 | const buttons = [ 52 | { 53 | content: , 54 | action: async () => await remote.skipToPrevious(), 55 | }, 56 | { 57 | content: , 58 | action: async () => await remote.skipToNext(), 59 | } 60 | ]; 61 | // Put play/pause into the middle of the array 62 | buttons.splice(buttons.length / 2, 0, isPaused ? 63 | { 64 | content: , 65 | action: async () => await remote.resume() 66 | } 67 | : 68 | { 69 | content: , 70 | action: async () => await remote.pause() 71 | } 72 | ); 73 | 74 | return ( 75 | 76 | 77 | {name} 78 | {artistName !== "" && {artistName}} 79 | 80 | 81 | 82 | {displayDuration(playbackPosition)} 83 | { 88 | remote.seek(Math.round(val)); 89 | }} 90 | style={{ flex: 1 }} 91 | /> 92 | {displayDuration(duration)} 93 | 94 | 95 | 96 | 97 | {buttons.map(({ content, action }, index) => ( 98 | 113 | ))} 114 | 115 | 122 | 128 | { 132 | remote.setShuffling(val) 133 | }} 134 | /> 135 | Shuffle 136 | 137 | 151 | 152 | 153 | 154 | 168 | 169 | ) 170 | } 171 | 172 | export default TransportControls; -------------------------------------------------------------------------------- /example/Readme.md: -------------------------------------------------------------------------------- 1 | # React Native Spotify Remote Example Project 2 | 3 | This project was bootstrapped using the following command: 4 | 5 | ```sh 6 | react-native init MyAwesomeProject --template typescript 7 | ``` 8 | 9 | Which came from https://facebook.github.io/react-native/blog/2018/05/07/using-typescript-with-react-native. 10 | 11 | 12 | ## Getting Started 13 | 14 | ### 0: Install Dependencies & Build Library 15 | 16 | 1. Run `yarn install` in this directory. 17 | 18 | 2. Install Pods by running: `cd ios && pod install && cd ..` 19 | 20 | 3. Run `yarn install && yarn build` in the [root directory](../) (`../`) to generate the library's `dist` folder. 21 | 22 | ### 1: ENV File 23 | 24 | I've also added `react-native-dotenv` for easier configuration. In order to configure this app you'll need to create a `.env` file in this root directory with the following: 25 | 26 | ```env 27 | SPOTIFY_CLIENT_ID="client_id_from_spotify_dashboard" 28 | SPOTIFY_REDIRECT_URL="redirect_uri_registered_in_spotify_dashboard" 29 | SPOTIFY_TOKEN_REFRESH_URL="http://{MACHINE_IP_ADDRESS}:3000/refresh" 30 | SPOTIFY_TOKEN_SWAP_URL="http://{MACHINE_IP_ADDRESS}:3000/swap" 31 | ``` 32 | 33 | > NOTE: If the changes to the environment variables aren't showing up in the app try and run `yarn start --reset-cache` to reset the packager cache 34 | 35 | > Note 1: The last two entries define the auth server endpoints. You can run the [example auth server](../example-server/README.md) for this purpose. 36 | 37 | > Note 2: The `SPOTIFY_REDIRECT_URI` needs to be both registered with your app on the Spotify Dashboard and also needs to be supported by your app. Instructions for doing this can be found on the [Spotify iOS Quickstart](https://developer.spotify.com/documentation/ios/quick-start/#setup-the-ios-sdk) in the **► Configure Info.plist** section 38 | 39 | ### 2. Run Server 40 | You will need to have the [Example Server](../example-server/README.md) running before your app starts up. 41 | 42 | ### 3: Start XCode Build 43 | Open up the `project.pbxproj` in XCode and build it for your phone (note you will need to have Spotify installed on your phone). 44 | > This should also start the react native packager as part of the build process if it isn't already running 45 | -------------------------------------------------------------------------------- /example/__tests__/App-test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); -------------------------------------------------------------------------------- /example/android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.example", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.example", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | 5 | /** 6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 7 | * and bundleReleaseJsAndAssets). 8 | * These basically call `react-native bundle` with the correct arguments during the Android build 9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 10 | * bundle directly from the development server. Below you can see all the possible configurations 11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 12 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 13 | * 14 | * project.ext.react = [ 15 | * // the name of the generated asset file containing your JS bundle 16 | * bundleAssetName: "index.android.bundle", 17 | * 18 | * // the entry file for bundle generation. If none specified and 19 | * // "index.android.js" exists, it will be used. Otherwise "index.js" is 20 | * // default. Can be overridden with ENTRY_FILE environment variable. 21 | * entryFile: "index.android.js", 22 | * 23 | * // https://reactnative.dev/docs/performance#enable-the-ram-format 24 | * bundleCommand: "ram-bundle", 25 | * 26 | * // whether to bundle JS and assets in debug mode 27 | * bundleInDebug: false, 28 | * 29 | * // whether to bundle JS and assets in release mode 30 | * bundleInRelease: true, 31 | * 32 | * // whether to bundle JS and assets in another build variant (if configured). 33 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 34 | * // The configuration property can be in the following formats 35 | * // 'bundleIn${productFlavor}${buildType}' 36 | * // 'bundleIn${buildType}' 37 | * // bundleInFreeDebug: true, 38 | * // bundleInPaidRelease: true, 39 | * // bundleInBeta: true, 40 | * 41 | * // whether to disable dev mode in custom build variants (by default only disabled in release) 42 | * // for example: to disable dev mode in the staging build type (if configured) 43 | * devDisabledInStaging: true, 44 | * // The configuration property can be in the following formats 45 | * // 'devDisabledIn${productFlavor}${buildType}' 46 | * // 'devDisabledIn${buildType}' 47 | * 48 | * // the root of your project, i.e. where "package.json" lives 49 | * root: "../../", 50 | * 51 | * // where to put the JS bundle asset in debug mode 52 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 53 | * 54 | * // where to put the JS bundle asset in release mode 55 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 56 | * 57 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 58 | * // require('./image.png')), in debug mode 59 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 60 | * 61 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 62 | * // require('./image.png')), in release mode 63 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 64 | * 65 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 66 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 67 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 68 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 69 | * // for example, you might want to remove it from here. 70 | * inputExcludes: ["android/**", "ios/**"], 71 | * 72 | * // override which node gets called and with what additional arguments 73 | * nodeExecutableAndArgs: ["node"], 74 | * 75 | * // supply additional arguments to the packager 76 | * extraPackagerArgs: [] 77 | * ] 78 | */ 79 | 80 | project.ext.react = [ 81 | enableHermes: false, // clean and rebuild if changing 82 | ] 83 | 84 | apply from: "../../node_modules/react-native/react.gradle" 85 | 86 | /** 87 | * Set this to true to create two separate APKs instead of one: 88 | * - An APK that only works on ARM devices 89 | * - An APK that only works on x86 devices 90 | * The advantage is the size of the APK is reduced by about 4MB. 91 | * Upload all the APKs to the Play Store and people will download 92 | * the correct one based on the CPU architecture of their device. 93 | */ 94 | def enableSeparateBuildPerCPUArchitecture = false 95 | 96 | /** 97 | * Run Proguard to shrink the Java bytecode in release builds. 98 | */ 99 | def enableProguardInReleaseBuilds = false 100 | 101 | /** 102 | * The preferred build flavor of JavaScriptCore. 103 | * 104 | * For example, to use the international variant, you can use: 105 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 106 | * 107 | * The international variant includes ICU i18n library and necessary data 108 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 109 | * give correct results when using with locales other than en-US. Note that 110 | * this variant is about 6MiB larger per architecture than default. 111 | */ 112 | def jscFlavor = 'org.webkit:android-jsc:+' 113 | 114 | /** 115 | * Whether to enable the Hermes VM. 116 | * 117 | * This should be set on project.ext.react and mirrored here. If it is not set 118 | * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode 119 | * and the benefits of using Hermes will therefore be sharply reduced. 120 | */ 121 | def enableHermes = project.ext.react.get("enableHermes", false); 122 | 123 | android { 124 | compileSdkVersion rootProject.ext.compileSdkVersion 125 | 126 | compileOptions { 127 | sourceCompatibility JavaVersion.VERSION_1_8 128 | targetCompatibility JavaVersion.VERSION_1_8 129 | } 130 | 131 | defaultConfig { 132 | applicationId "com.react.native.spotify.remote.example" 133 | minSdkVersion rootProject.ext.minSdkVersion 134 | targetSdkVersion rootProject.ext.targetSdkVersion 135 | versionCode 1 136 | versionName "1.0" 137 | } 138 | splits { 139 | abi { 140 | reset() 141 | enable enableSeparateBuildPerCPUArchitecture 142 | universalApk false // If true, also generate a universal APK 143 | include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" 144 | } 145 | } 146 | signingConfigs { 147 | debug { 148 | storeFile file('./debug.keystore') 149 | storePassword 'android' 150 | keyAlias 'androiddebugkey' 151 | keyPassword 'android' 152 | } 153 | } 154 | buildTypes { 155 | debug { 156 | signingConfig signingConfigs.debug 157 | } 158 | release { 159 | // Caution! In production, you need to generate your own keystore file. 160 | // see https://reactnative.dev/docs/signed-apk-android. 161 | signingConfig signingConfigs.debug 162 | minifyEnabled enableProguardInReleaseBuilds 163 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 164 | } 165 | } 166 | 167 | packagingOptions { 168 | pickFirst "lib/armeabi-v7a/libc++_shared.so" 169 | pickFirst "lib/arm64-v8a/libc++_shared.so" 170 | pickFirst "lib/x86/libc++_shared.so" 171 | pickFirst "lib/x86_64/libc++_shared.so" 172 | } 173 | 174 | // applicationVariants are e.g. debug, release 175 | applicationVariants.all { variant -> 176 | variant.outputs.each { output -> 177 | // For each separate APK per architecture, set a unique version code as described here: 178 | // https://developer.android.com/studio/build/configure-apk-splits.html 179 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] 180 | def abi = output.getFilter(OutputFile.ABI) 181 | if (abi != null) { // null for the universal-debug, universal-release variants 182 | output.versionCodeOverride = 183 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 184 | } 185 | 186 | } 187 | } 188 | } 189 | 190 | dependencies { 191 | implementation fileTree(dir: "libs", include: ["*.jar"]) 192 | implementation project(':react-native-spotify-remote') 193 | implementation "com.facebook.react:react-native:+" // From node_modules 194 | 195 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" 196 | 197 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { 198 | exclude group:'com.facebook.fbjni' 199 | } 200 | 201 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { 202 | exclude group:'com.facebook.flipper' 203 | exclude group:'com.squareup.okhttp3', module:'okhttp' 204 | } 205 | 206 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") { 207 | exclude group:'com.facebook.flipper' 208 | } 209 | 210 | if (enableHermes) { 211 | def hermesPath = "../../node_modules/hermes-engine/android/"; 212 | debugImplementation files(hermesPath + "hermes-debug.aar") 213 | releaseImplementation files(hermesPath + "hermes-release.aar") 214 | } else { 215 | implementation jscFlavor 216 | } 217 | } 218 | 219 | // Run this once to be able to run the application with BUCK 220 | // puts all compile dependencies into folder libs for BUCK to use 221 | task copyDownloadableDepsToLibs(type: Copy) { 222 | from configurations.compile 223 | into 'libs' 224 | } 225 | 226 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) 227 | -------------------------------------------------------------------------------- /example/android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /example/android/app/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/android/app/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Apr 27 21:10:44 PDT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip 7 | -------------------------------------------------------------------------------- /example/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/debug/java/com/example/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.example; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 22 | import com.facebook.react.ReactInstanceManager; 23 | import com.facebook.react.bridge.ReactContext; 24 | import com.facebook.react.modules.network.NetworkingModule; 25 | import okhttp3.OkHttpClient; 26 | 27 | public class ReactNativeFlipper { 28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 29 | if (FlipperUtils.shouldEnableFlipper(context)) { 30 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 31 | 32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 33 | client.addPlugin(new ReactFlipperPlugin()); 34 | client.addPlugin(new DatabasesFlipperPlugin(context)); 35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 36 | client.addPlugin(CrashReporterPlugin.getInstance()); 37 | 38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 39 | NetworkingModule.setCustomClientBuilder( 40 | new NetworkingModule.CustomClientBuilder() { 41 | @Override 42 | public void apply(OkHttpClient.Builder builder) { 43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 44 | } 45 | }); 46 | client.addPlugin(networkFlipperPlugin); 47 | client.start(); 48 | 49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 50 | // Hence we run if after all native modules have been initialized 51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 52 | if (reactContext == null) { 53 | reactInstanceManager.addReactInstanceEventListener( 54 | new ReactInstanceManager.ReactInstanceEventListener() { 55 | @Override 56 | public void onReactContextInitialized(ReactContext reactContext) { 57 | reactInstanceManager.removeReactInstanceEventListener(this); 58 | reactContext.runOnNativeModulesQueueThread( 59 | new Runnable() { 60 | @Override 61 | public void run() { 62 | client.addPlugin(new FrescoFlipperPlugin()); 63 | } 64 | }); 65 | } 66 | }); 67 | } else { 68 | client.addPlugin(new FrescoFlipperPlugin()); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/AntDesign.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/AntDesign.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/Entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/Entypo.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/EvilIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/EvilIcons.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/Feather.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/Fontisto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/Fontisto.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/Foundation.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/Foundation.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/Ionicons.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/Octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/Octicons.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/Roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/Roboto.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/Roboto_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/Roboto_medium.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/SimpleLineIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/SimpleLineIcons.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/Zocial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/Zocial.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/assets/fonts/rubicon-icon-font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/assets/fonts/rubicon-icon-font.ttf -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. This is used to schedule 9 | * rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "example"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | import android.app.Application; 3 | import android.content.Context; 4 | import com.facebook.react.PackageList; 5 | import com.facebook.react.ReactApplication; 6 | import com.facebook.react.ReactInstanceManager; 7 | import com.facebook.react.ReactNativeHost; 8 | import com.facebook.react.ReactPackage; 9 | import com.facebook.soloader.SoLoader; 10 | import com.reactlibrary.RNSpotifyRemotePackage; 11 | 12 | import java.lang.reflect.InvocationTargetException; 13 | import java.util.List; 14 | 15 | public class MainApplication extends Application implements ReactApplication { 16 | 17 | private final ReactNativeHost mReactNativeHost = 18 | new ReactNativeHost(this) { 19 | @Override 20 | public boolean getUseDeveloperSupport() { 21 | return BuildConfig.DEBUG; 22 | } 23 | 24 | @Override 25 | protected List getPackages() { 26 | List packages = new PackageList(this).getPackages(); 27 | 28 | // To allow for easier development, add manually. 29 | // Not necessary in the wild as it will be autolinked 30 | packages.add(new RNSpotifyRemotePackage()); 31 | return packages; 32 | } 33 | 34 | @Override 35 | protected String getJSMainModuleName() { 36 | return "index"; 37 | } 38 | }; 39 | 40 | @Override 41 | public ReactNativeHost getReactNativeHost() { 42 | return mReactNativeHost; 43 | } 44 | 45 | @Override 46 | public void onCreate() { 47 | super.onCreate(); 48 | SoLoader.init(this, /* native exopackage */ false); 49 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 50 | } 51 | 52 | /** 53 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 54 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 55 | * 56 | * @param context 57 | * @param reactInstanceManager 58 | */ 59 | private static void initializeFlipper( 60 | Context context, ReactInstanceManager reactInstanceManager) { 61 | if (BuildConfig.DEBUG) { 62 | try { 63 | /* 64 | We use reflection here to pick up the class that initializes Flipper, 65 | since Flipper library is not available in release mode 66 | */ 67 | Class aClass = Class.forName("com.example.ReactNativeFlipper"); 68 | aClass 69 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 70 | .invoke(null, context, reactInstanceManager); 71 | } catch (ClassNotFoundException e) { 72 | e.printStackTrace(); 73 | } catch (NoSuchMethodException e) { 74 | e.printStackTrace(); 75 | } catch (IllegalAccessException e) { 76 | e.printStackTrace(); 77 | } catch (InvocationTargetException e) { 78 | e.printStackTrace(); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | React Native Spotify Remote Example 3 | 4 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "29.0.2" 6 | minSdkVersion = 16 7 | compileSdkVersion = 29 8 | targetSdkVersion = 29 9 | } 10 | repositories { 11 | google() 12 | jcenter() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle:3.6.3") 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | mavenLocal() 25 | maven { 26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 27 | url("$rootDir/../node_modules/react-native/android") 28 | } 29 | maven { 30 | // Android JSC is installed from npm 31 | url("$rootDir/../node_modules/jsc-android/dist") 32 | } 33 | 34 | google() 35 | jcenter() 36 | maven { url 'https://www.jitpack.io' } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.54.0 -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjam/react-native-spotify-remote/339933b713d2b62f016cf3f42aa3984e0d85efad/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Spotify Remote SDK Example' 2 | 3 | include ':react-native-spotify-remote' 4 | project(':react-native-spotify-remote').projectDir = new File(rootProject.projectDir, '../../android') 5 | 6 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 7 | 8 | include ':app' 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "displayName": "example" 4 | } -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = { 5 | presets: [ 6 | 'module:metro-react-native-babel-preset', 7 | 'module:react-native-dotenv' 8 | ], 9 | plugins: [ 10 | [ 11 | 'module-resolver', 12 | { 13 | alias: { 14 | [pak.name]: path.join(__dirname, '../src/'), 15 | }, 16 | }, 17 | ], 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './App'; 7 | import {name as appName} from './app.json'; 8 | 9 | AppRegistry.registerComponent(appName, () => App); 10 | -------------------------------------------------------------------------------- /example/ios/Launch Screen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '10.0' 2 | 3 | node_modules_path = '../node_modules' 4 | root_node_modules_path = '../../node_modules' 5 | require_relative "#{node_modules_path}/react-native/scripts/react_native_pods" 6 | require_relative "#{node_modules_path}/@react-native-community/cli-platform-ios/native_modules" 7 | 8 | target 'example' do 9 | config = use_native_modules! 10 | 11 | use_react_native!(:path => config["reactNativePath"]) 12 | 13 | # Want to install the pod from the source directory so we can change it while developing 14 | pod 'RNSpotifyRemote', :path => "../../" 15 | 16 | # Enables Flipper. 17 | # 18 | # Note that if you have use_frameworks! enabled, Flipper will not work and 19 | # you should disable these next few lines. 20 | # Need to specify flipper versions due to issue with XCode 12.5 21 | # See https://github.com/facebook/flipper/issues/2215#issuecomment-828837100 22 | use_flipper!({ 'Flipper-Folly' => '2.5.3', 'Flipper' => '0.87.0', 'Flipper-RSocket' => '1.3.1' }) 23 | post_install do |installer| 24 | flipper_post_install(installer) 25 | end 26 | end -------------------------------------------------------------------------------- /example/ios/example-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /example/ios/example.xcodeproj/xcshareddata/xcschemes/example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 65 | 66 | 67 | 68 | 70 | 76 | 77 | 78 | 79 | 80 | 90 | 92 | 98 | 99 | 100 | 101 | 107 | 109 | 115 | 116 | 117 | 118 | 120 | 121 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /example/ios/example.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/example/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /example/ios/example/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | #import 7 | 8 | #if DEBUG 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | 16 | static void InitializeFlipper(UIApplication *application) { 17 | FlipperClient *client = [FlipperClient sharedClient]; 18 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; 19 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; 20 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; 21 | [client addPlugin:[FlipperKitReactPlugin new]]; 22 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; 23 | [client start]; 24 | } 25 | #endif 26 | 27 | 28 | @implementation AppDelegate 29 | 30 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 31 | { 32 | 33 | #if DEBUG 34 | InitializeFlipper(application); 35 | #endif 36 | 37 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 38 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 39 | moduleName:@"example" 40 | initialProperties:nil]; 41 | 42 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 43 | 44 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 45 | UIViewController *rootViewController = [UIViewController new]; 46 | rootViewController.view = rootView; 47 | self.window.rootViewController = rootViewController; 48 | [self.window makeKeyAndVisible]; 49 | return YES; 50 | } 51 | 52 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 53 | { 54 | #if DEBUG 55 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 56 | #else 57 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 58 | #endif 59 | } 60 | 61 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)URL options:(NSDictionary *)options 62 | { 63 | return [[RNSpotifyRemoteAuth sharedInstance] application:application openURL:URL options:options]; 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /example/ios/example/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /example/ios/example/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | RN Spotify Remote Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleURLName 27 | $(PRODUCT_BUNDLE_IDENTIFIER) 28 | CFBundleURLSchemes 29 | 30 | example 31 | 32 | 33 | 34 | CFBundleVersion 35 | 1 36 | LSApplicationQueriesSchemes 37 | 38 | spotify 39 | 40 | LSRequiresIPhoneOS 41 | 42 | NSAppTransportSecurity 43 | 44 | NSAllowsArbitraryLoads 45 | 46 | NSExceptionDomains 47 | 48 | localhost 49 | 50 | NSExceptionAllowsInsecureHTTPLoads 51 | 52 | 53 | 54 | 55 | NSLocationWhenInUseUsageDescription 56 | 57 | UIAppFonts 58 | 59 | AntDesign.ttf 60 | Entypo.ttf 61 | EvilIcons.ttf 62 | Feather.ttf 63 | FontAwesome.ttf 64 | FontAwesome5_Brands.ttf 65 | FontAwesome5_Regular.ttf 66 | FontAwesome5_Solid.ttf 67 | Fontisto.ttf 68 | Foundation.ttf 69 | Ionicons.ttf 70 | MaterialCommunityIcons.ttf 71 | MaterialIcons.ttf 72 | Octicons.ttf 73 | Roboto_medium.ttf 74 | Roboto.ttf 75 | rubicon-icon-font.ttf 76 | SimpleLineIcons.ttf 77 | Zocial.ttf 78 | 79 | UILaunchStoryboardName 80 | Launch Screen 81 | UIRequiredDeviceCapabilities 82 | 83 | armv7 84 | 85 | UISupportedInterfaceOrientations 86 | 87 | UIInterfaceOrientationPortrait 88 | UIInterfaceOrientationLandscapeLeft 89 | UIInterfaceOrientationLandscapeRight 90 | UIInterfaceOrientationPortraitUpsideDown 91 | 92 | UIViewControllerBasedStatusBarAppearance 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /example/ios/example/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/libs/react-native-dotenv.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-native-dotenv" { 2 | export const SPOTIFY_CLIENT_ID: string; 3 | export const SPOTIFY_REDIRECT_URL: string; 4 | export const SPOTIFY_TOKEN_REFRESH_URL: string; 5 | export const SPOTIFY_TOKEN_SWAP_URL: string; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const blacklist = require('metro-config/src/defaults/blacklist'); 3 | const escape = require('escape-string-regexp'); 4 | const pak = require('../package.json'); 5 | 6 | const root = path.resolve(__dirname, '..'); 7 | 8 | const modules = Object.keys({ 9 | ...pak.peerDependencies, 10 | }); 11 | 12 | module.exports = { 13 | projectRoot: __dirname, 14 | watchFolders: [root], 15 | 16 | // We need to make sure that only one version is loaded for peerDependencies 17 | // So we blacklist them at the root, and alias them to the versions in example's node_modules 18 | resolver: { 19 | blacklistRE: blacklist( 20 | modules.map( 21 | (m) => 22 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 23 | ) 24 | ), 25 | 26 | extraNodeModules: modules.reduce((acc, name) => { 27 | acc[name] = path.join(__dirname, 'node_modules', name); 28 | return acc; 29 | }, {}), 30 | }, 31 | 32 | transformer: { 33 | getTransformOptions: async () => ({ 34 | transform: { 35 | experimentalImportSupport: false, 36 | inlineRequires: true, 37 | }, 38 | }), 39 | }, 40 | }; 41 | 42 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "test": "jest", 10 | "lint": "eslint ." 11 | }, 12 | "dependencies": { 13 | "@react-native-community/slider": "^2.0.8", 14 | "luxon": "^1.21.3", 15 | "native-base": "^2.13.12", 16 | "react": "16.13.1", 17 | "react-native": "0.63.4" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.9.6", 21 | "@babel/runtime": "^7.9.6", 22 | "@react-native-community/eslint-config": "^1.1.0", 23 | "@types/jest": "^24.0.23", 24 | "@types/luxon": "^1.21.0", 25 | "@types/react": "^16.9.16", 26 | "@types/react-native": "^0.60.25", 27 | "@types/react-test-renderer": "^16.9.1", 28 | "babel-jest": "^25.5.1", 29 | "babel-plugin-module-resolver": "^4.1.0", 30 | "eslint": "^6.8.0", 31 | "jest": "^25.5.2", 32 | "metro-react-native-babel-preset": "^0.59.0", 33 | "patch-package": "^6.2.2", 34 | "react-native-dotenv": "^0.2.0", 35 | "react-test-renderer": "16.13.1" 36 | }, 37 | "jest": { 38 | "preset": "react-native" 39 | } 40 | } -------------------------------------------------------------------------------- /example/scripts/examples_postinstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * Using libraries within examples and linking them within packages.json like: 5 | * "react-native-library-name": "file:../" 6 | * will cause problems with the metro bundler if the example will run via 7 | * `react-native run-[ios|android]`. This will result in an error as the metro 8 | * bundler will find multiple versions for the same module while resolving it. 9 | * The reason for that is that if the library is installed it also copies in the 10 | * example folder itself as well as the node_modules folder of the library 11 | * although their are defined in .npmignore and should be ignored in theory. 12 | * 13 | * This postinstall script removes the node_modules folder as well as all 14 | * entries from the libraries .npmignore file within the examples node_modules 15 | * folder after the library was installed. This should resolve the metro 16 | * bundler issue mentioned above. 17 | * 18 | * It is expected this scripts lives in the libraries root folder within a 19 | * scripts folder. As first parameter the relative path to the libraries 20 | * folder within the example's node_modules folder may be provided. 21 | * This script will determine the path from this project's package.json file 22 | * if no such relative path is provided. 23 | * An example's package.json entry could look like: 24 | * "postinstall": "node ../scripts/examples_postinstall.js node_modules/react-native-library-name/" 25 | */ 26 | 27 | 'use strict'; 28 | 29 | const fs = require('fs'); 30 | const path = require('path'); 31 | 32 | /// Delete all files and directories for the given path 33 | const removeFileDirectoryRecursively = fileDirPath => { 34 | // Remove file 35 | if (!fs.lstatSync(fileDirPath).isDirectory()) { 36 | fs.unlinkSync(fileDirPath); 37 | return; 38 | } 39 | 40 | // Go down the directory an remove each file / directory recursively 41 | fs.readdirSync(fileDirPath).forEach(entry => { 42 | const entryPath = path.join(fileDirPath, entry); 43 | removeFileDirectoryRecursively(entryPath); 44 | }); 45 | fs.rmdirSync(fileDirPath); 46 | }; 47 | 48 | /// Remove example/node_modules/react-native-library-name/node_modules directory 49 | const removeLibraryNodeModulesPath = (libraryNodeModulesPath) => { 50 | const nodeModulesPath = path.resolve(libraryNodeModulesPath, 'node_modules') 51 | 52 | if (!fs.existsSync(nodeModulesPath)) { 53 | console.log(`No node_modules path found at ${nodeModulesPath}. Skipping delete.`) 54 | return; 55 | } 56 | 57 | console.log(`Deleting: ${nodeModulesPath}`) 58 | try { 59 | removeFileDirectoryRecursively(nodeModulesPath); 60 | console.log(`Successfully deleted: ${nodeModulesPath}`) 61 | } catch (err) { 62 | console.log(`Error deleting ${nodeModulesPath}: ${err.message}`); 63 | } 64 | }; 65 | 66 | /// Remove all entries from the .npmignore within example/node_modules/react-native-library-name/ 67 | const removeLibraryNpmIgnorePaths = (npmIgnorePath, libraryNodeModulesPath) => { 68 | if (!fs.existsSync(npmIgnorePath)) { 69 | console.log(`No .npmignore path found at ${npmIgnorePath}. Skipping deleting content.`); 70 | return; 71 | } 72 | 73 | fs.readFileSync(npmIgnorePath, 'utf8').split(/\r?\n/).forEach(entry => { 74 | if (entry.length === 0) { 75 | return 76 | } 77 | 78 | const npmIgnoreLibraryNodeModulesEntryPath = path.resolve(libraryNodeModulesPath, entry); 79 | if (!fs.existsSync(npmIgnoreLibraryNodeModulesEntryPath)) { 80 | return; 81 | } 82 | 83 | console.log(`Deleting: ${npmIgnoreLibraryNodeModulesEntryPath}`) 84 | try { 85 | removeFileDirectoryRecursively(npmIgnoreLibraryNodeModulesEntryPath); 86 | console.log(`Successfully deleted: ${npmIgnoreLibraryNodeModulesEntryPath}`) 87 | } catch (err) { 88 | console.log(`Error deleting ${npmIgnoreLibraryNodeModulesEntryPath}: ${err.message}`); 89 | } 90 | }); 91 | }; 92 | 93 | // Main start sweeping process 94 | const main = () => { 95 | // Read out dir of example project 96 | const exampleDir = process.cwd(); 97 | 98 | console.log(`Starting postinstall cleanup for ${exampleDir}`); 99 | 100 | // Resolve the React Native library's path within the example's node_modules directory 101 | const libraryNodeModulesPath = process.argv.length > 2 102 | ? path.resolve(exampleDir, process.argv[2]) 103 | : path.resolve(exampleDir, 'node_modules', require('../../package.json').name); 104 | 105 | console.log(`Removing unwanted artifacts for ${libraryNodeModulesPath}`); 106 | 107 | removeLibraryNodeModulesPath(libraryNodeModulesPath); 108 | 109 | const npmIgnorePath = path.resolve(__dirname, '../../.npmignore'); 110 | removeLibraryNpmIgnorePaths(npmIgnorePath, libraryNodeModulesPath); 111 | } 112 | 113 | // main(); 114 | -------------------------------------------------------------------------------- /example/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | const styles = StyleSheet.create({ 4 | actionButton: { 5 | borderColor: "#0000BB", 6 | borderWidth: 2, 7 | }, 8 | error: { 9 | color: "#AA0000", 10 | borderColor: "#AA0000", 11 | borderWidth: 2, 12 | fontSize: 18, 13 | padding: 10, 14 | margin: 10, 15 | marginTop: 20 16 | }, 17 | connected: { 18 | color: "#00AA00", 19 | fontSize: 16, 20 | }, 21 | disconnected: { 22 | color: "#880000", 23 | fontSize: 16 24 | }, 25 | container: { 26 | height: "100%", 27 | width: "100%", 28 | backgroundColor: '#F5FCFF', 29 | }, 30 | welcome: { 31 | fontSize: 20, 32 | textAlign: 'center', 33 | margin: 10, 34 | }, 35 | instructions: { 36 | textAlign: 'center', 37 | color: '#333333', 38 | marginBottom: 5, 39 | }, 40 | transportContainer: { 41 | display: 'flex', 42 | justifyContent: 'space-evenly', 43 | height: "100%", 44 | width: "100%", 45 | backgroundColor: "#EEE", 46 | borderColor: "#DDD", 47 | borderWidth: 1, 48 | paddingVertical: 5 49 | }, 50 | textDuration: { 51 | width: 40, 52 | }, 53 | textCentered: { 54 | textAlign: "center" 55 | }, 56 | debugBorder: { 57 | borderWidth: 1, 58 | borderColor: 'red' 59 | } 60 | }); 61 | 62 | export default styles; -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "isolatedModules": true, 7 | "jsx": "react", 8 | "lib": [ 9 | "es6" 10 | ], 11 | "moduleResolution": "node", 12 | "noEmit": true, 13 | "strict": true, 14 | "target": "esnext", 15 | "baseUrl": ".", 16 | "paths": { 17 | "react-native-spotify-remote": [ 18 | "../src/index" 19 | ] 20 | }, 21 | }, 22 | "exclude": [ 23 | "node_modules", 24 | "babel.config.js", 25 | "metro.config.js", 26 | "jest.config.js" 27 | ], 28 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Spotify from './dist/Spotify'; 2 | export default Spotify; -------------------------------------------------------------------------------- /ios/Macros.h: -------------------------------------------------------------------------------- 1 | // 2 | // Macros.h 3 | // RNSpotify 4 | // 5 | // Created by Colter McQuay on 2018-10-06. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #ifndef Macros_h 10 | #define Macros_h 11 | 12 | #ifdef DEBUG 13 | #define DLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__); 14 | #define IsDebug 1 15 | #else 16 | #define DLog(...) 17 | #define IsDebug 0 18 | #endif 19 | 20 | #endif /* Macros_h */ 21 | -------------------------------------------------------------------------------- /ios/NSArrayExtensions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSArrayExtensions.h 3 | // RNSpotify 4 | // 5 | // Created by Colter McQuay on 2018-10-11. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #ifndef NSArrayExtensions_h 10 | #define NSArrayExtensions_h 11 | 12 | typedef id(^MapBlock)(id); 13 | @interface NSArray (FP) 14 | - (NSArray *)map:(MapBlock)block; 15 | @end 16 | 17 | #endif /* NSArrayExtensions_h */ 18 | -------------------------------------------------------------------------------- /ios/NSArrayExtensions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSArrayExtensions.m 3 | // RNSpotify 4 | // 5 | // Created by Colter McQuay on 2018-10-11. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "NSArrayExtensions.h" 11 | 12 | @implementation NSArray (FP) 13 | - (NSArray *)map:(MapBlock)block { 14 | NSMutableArray *resultArray = [[NSMutableArray alloc] init]; 15 | for (id object in self) { 16 | [resultArray addObject:block(object)]; 17 | } 18 | return resultArray; 19 | } 20 | @end 21 | -------------------------------------------------------------------------------- /ios/RNSpotifyItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNSpotifyItem.h 3 | // RNSpotify 4 | // 5 | // Created by Colter McQuay on 2018-10-10. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #ifndef RNSpotifyItem_h 12 | #define RNSpotifyItem_h 13 | 14 | 15 | @interface RNSpotifyItem : NSObject 16 | - (id)initWithJson:(NSDictionary*) json; 17 | + (RNSpotifyItem*)fromJSON:(NSDictionary*) json; 18 | @end 19 | 20 | #endif /* RNSpotifyItem_h */ 21 | -------------------------------------------------------------------------------- /ios/RNSpotifyItem.m: -------------------------------------------------------------------------------- 1 | // 2 | // RNSpotifyItem.m 3 | // RNSpotify 4 | // 5 | // Created by Colter McQuay on 2018-10-10. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "./RNSpotifyItem.h" 11 | 12 | @implementation RNSpotifyItem 13 | 14 | @synthesize availableOffline; 15 | @synthesize container; 16 | @synthesize identifier; 17 | @synthesize imageIdentifier; 18 | @synthesize playable; 19 | @synthesize subtitle; 20 | @synthesize title; 21 | @synthesize URI; 22 | @synthesize children; 23 | 24 | -(id)initWithJson:(NSDictionary *)json{ 25 | if(self = [super init]) 26 | { 27 | availableOffline = [NSNull null]; 28 | container = [json[@"container"] boolValue]; 29 | identifier = json[@"id"] != nil ? json[@"id"] : [NSNull null]; 30 | imageIdentifier = nil; 31 | playable = [json[@"playable"] boolValue]; 32 | subtitle = json[@"subtitle"] != nil ? json[@"subtitle"] : [NSNull null]; 33 | title = json[@"title"] != nil ? json[@"title"] : [NSNull null]; 34 | URI = json[@"uri"] != nil ? json[@"uri"] : [NSNull null]; 35 | children = nil; 36 | } 37 | return self; 38 | } 39 | 40 | +(RNSpotifyItem*)fromJSON:(NSDictionary *)json{ 41 | return [[RNSpotifyItem alloc] initWithJson:json]; 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemote.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNSpotifyRemoteLib.h 3 | // RNSpotifyRemote 4 | // 5 | // Created by Colter McQuay on 2019-03-08. 6 | // Copyright © 2019 Facebook. All rights reserved. 7 | // 8 | 9 | #ifndef RNSpotifyRemoteLib_h 10 | #define RNSpotifyRemoteLib_h 11 | 12 | #import "RNSpotifyRemoteAuth.h" 13 | #import "RNSpotifyRemoteAppRemote.h" 14 | 15 | #endif /* RNSpotifyRemoteLib_h */ 16 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemote.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemoteAppRemote.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface RNSpotifyRemoteAppRemote : RCTEventEmitter 5 | 6 | //// isInitialized 7 | //-(id)isInitialized; 8 | 9 | +(instancetype)sharedInstance; 10 | 11 | -(void)isConnectedAsync:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 12 | 13 | -(void)disconnect; 14 | -(void)disconnect:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 15 | -(void)connect:(NSString*)accessToken resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 16 | 17 | // Playback API 18 | -(void)playUri:(NSString*)uri resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 19 | -(void)playItem:(NSDictionary*)item resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 20 | -(void)playItemWithIndex:(NSDictionary*)item skipToTrackIndex:(NSInteger)skipToTrackIndex resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 21 | -(void)queueUri: (NSString*)uri resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 22 | -(void)resume:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 23 | -(void)pause:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 24 | -(void)skipToNext:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 25 | -(void)skipToPrevious:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 26 | -(void)seek: (NSInteger)uri resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 27 | 28 | -(void)setShuffling: (BOOL)shuffling resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 29 | -(void)setRepeatMode: (NSInteger)repeatMode resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 30 | 31 | -(void)getPlayerState:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 32 | -(void)getCrossfadeState:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 33 | 34 | // Content API 35 | -(void)getRootContentItems:(NSString*)type resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 36 | -(void)getRecommendedContentItems:(NSDictionary*)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 37 | -(void)getChildrenOfItem:(NSDictionary*)item resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 38 | -(void)getContentItemForUri:(NSString *)uri resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 39 | 40 | // Internal events API 41 | -(void)eventStartObserving:(NSString *)eventType; 42 | -(void)eventStopObserving:(NSString *)eventType; 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemoteAuth.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import "RNSpotifyRemotePromise.h" 5 | 6 | @interface RNSpotifyRemoteAuth : NSObject 7 | 8 | -(BOOL)application:(UIApplication *)application openURL:(NSURL *)URL options:(NSDictionary *)options; 9 | 10 | +(instancetype)sharedInstance; 11 | 12 | -(void)authorize:(NSDictionary*)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 13 | -(void)endSession:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 14 | -(void)getSession:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; 15 | -(BOOL)isSpotifyInstalled; 16 | 17 | -(SPTConfiguration*) configuration; 18 | -(NSString*) accessToken; 19 | @end 20 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemoteConvert.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | #import 4 | #import "RNSpotifyRemoteError.h" 5 | 6 | @interface RNSpotifyRemoteConvert : NSObject 7 | 8 | +(id)ID:(id)obj; 9 | +(NSString*)Date:(NSDate*)date; 10 | +(id)RNSpotifyError:(RNSpotifyRemoteError*)error; 11 | +(id)NSError:(NSError*)error; 12 | +(id)SPTAppRemotePlayerState:(NSObject*) state; 13 | +(id)SPTAppRemoteCrossfadeState:(NSObject*) state; 14 | +(id)SPTAppRemotePlaybackRestrictions:(NSObject*) restrictions; 15 | +(id)SPTAppRemotePlaybackOptions:(NSObject*) options; 16 | +(id)SPTAppRemoteTrack:(NSObject *) track; 17 | +(id)SPTAppRemoteArtist:(NSObject *) artist; 18 | +(id)SPTAppRemoteAlbum:(NSObject *) album; 19 | +(id)SPTAppRemoteContentItem:(NSObject *) item; 20 | +(id)SPTAppRemoteContentItems:(NSArray *) items; 21 | +(id)SPTSession:(SPTSession *) session; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemoteConvert.m: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | #import "RNSpotifyRemoteConvert.h" 4 | #import "NSArrayExtensions.h" 5 | 6 | 7 | @interface RNSpotifyRemoteConvert() 8 | +(NSDateFormatter *) ISO_DATE_FORMATTER; 9 | @end 10 | 11 | 12 | 13 | @implementation RNSpotifyRemoteConvert 14 | 15 | +(id)ID:(id)obj 16 | { 17 | if(obj == nil) 18 | { 19 | return [NSNull null]; 20 | } 21 | return obj; 22 | } 23 | 24 | 25 | static NSDateFormatter* _ISO_DATE_FORMATTER; 26 | +(NSDateFormatter*) ISO_DATE_FORMATTER{ 27 | if(_ISO_DATE_FORMATTER == nil){ 28 | NSDateFormatter * dateFormatter = [[NSDateFormatter alloc] init]; 29 | // setLocale importance https://developer.apple.com/library/archive/qa/qa1480/_index.html 30 | [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]]; 31 | [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"]; 32 | [dateFormatter setCalendar:[NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]]; 33 | _ISO_DATE_FORMATTER = dateFormatter; 34 | } 35 | 36 | return _ISO_DATE_FORMATTER; 37 | } 38 | 39 | +(NSString*)Date:(NSDate *) date{ 40 | return [[self ISO_DATE_FORMATTER] stringFromDate:date]; 41 | } 42 | 43 | +(id)RNSpotifyError:(RNSpotifyRemoteError*)error 44 | { 45 | if(error==nil) 46 | { 47 | return [NSNull null]; 48 | } 49 | return error.reactObject; 50 | } 51 | 52 | +(id)NSError:(NSError*)error 53 | { 54 | if(error==nil) 55 | { 56 | return [NSNull null]; 57 | } 58 | return [self RNSpotifyError:[RNSpotifyRemoteError errorWithNSError:error]]; 59 | } 60 | 61 | +(id)SPTAppRemoteCrossfadeState:(NSObject *)state{ 62 | if(state == nil) 63 | { 64 | return [NSNull null]; 65 | } 66 | return @{ 67 | @"duration": [NSNumber numberWithInteger:state.duration], 68 | @"enabled": [NSNumber numberWithBool:state.enabled], 69 | }; 70 | } 71 | 72 | +(id)SPTAppRemotePlayerState:(NSObject*) state{ 73 | if(state == nil) 74 | { 75 | return [NSNull null]; 76 | } 77 | return @{ 78 | @"state": @{ 79 | @"track": [RNSpotifyRemoteConvert SPTAppRemoteTrack:state.track], 80 | @"playbackPosition": [NSNumber numberWithInteger:state.playbackPosition], 81 | @"playbackSpeed": [NSNumber numberWithFloat:state.playbackSpeed], 82 | @"isPaused": [NSNumber numberWithBool:state.isPaused], 83 | @"playbackRestrictions": [RNSpotifyRemoteConvert SPTAppRemotePlaybackRestrictions:state.playbackRestrictions], 84 | @"playbackOptions": [RNSpotifyRemoteConvert SPTAppRemotePlaybackOptions:state.playbackOptions] 85 | }, 86 | @"context": @{ 87 | @"title": state.contextTitle ? state.contextTitle : @"", 88 | @"uri": state.contextURI 89 | } 90 | }; 91 | } 92 | 93 | +(id)SPTAppRemotePlaybackRestrictions:(NSObject*) restrictions{ 94 | if(restrictions == nil){ 95 | return [NSNull null]; 96 | } 97 | 98 | return @{ 99 | @"canSkipNext": [NSNumber numberWithBool:restrictions.canSkipNext], 100 | @"canSkipPrevious": [NSNumber numberWithBool:restrictions.canSkipPrevious], 101 | @"canRepeatTrack": [NSNumber numberWithBool:restrictions.canRepeatTrack], 102 | @"canRepeatContext": [NSNumber numberWithBool:restrictions.canRepeatContext], 103 | @"canToggleShuffle": [NSNumber numberWithBool:restrictions.canToggleShuffle], 104 | @"canSeek":[NSNumber numberWithBool:restrictions.canSeek] 105 | }; 106 | } 107 | 108 | +(id)SPTAppRemotePlaybackOptions:(NSObject*) options{ 109 | if(options == nil){ 110 | return [NSNull null]; 111 | } 112 | return @{ 113 | @"isShuffling": [NSNumber numberWithBool:options.isShuffling], 114 | @"repeatMode": [NSNumber numberWithUnsignedInteger:options.repeatMode], 115 | }; 116 | } 117 | 118 | 119 | +(id)SPTAppRemoteTrack:(NSObject*) track{ 120 | if(track == nil){ 121 | return [NSNull null]; 122 | } 123 | return @{ 124 | @"name": track.name, 125 | @"uri":track.URI, 126 | @"duration":[NSNumber numberWithUnsignedInteger:track.duration], 127 | @"artist":[RNSpotifyRemoteConvert SPTAppRemoteArtist:track.artist], 128 | @"album":[RNSpotifyRemoteConvert SPTAppRemoteAlbum:track.album], 129 | @"saved":[NSNumber numberWithBool:track.saved], 130 | @"episode":[NSNumber numberWithBool:track.episode], 131 | @"podcast":[NSNumber numberWithBool:track.podcast], 132 | }; 133 | } 134 | 135 | +(id)SPTAppRemoteArtist:(NSObject*) artist{ 136 | if(artist == nil){ 137 | return [NSNull null]; 138 | } 139 | NSString *artistName = @""; 140 | if (artist.name != nil) { 141 | artistName = artist.name; 142 | } 143 | NSString *artistUri = @""; 144 | if (artist.URI != nil) { 145 | artistUri = artist.URI; 146 | } 147 | 148 | return @{ 149 | @"name":artist.name == nil ? @"" : artist.name, 150 | @"uri":artist.URI == nil ? @"" : artist.URI 151 | }; 152 | } 153 | 154 | +(id)SPTAppRemoteAlbum:(NSObject*) album{ 155 | if(album == nil){ 156 | return [NSNull null]; 157 | } 158 | NSString *albumName = @""; 159 | if (album.name != nil) { 160 | albumName = album.name; 161 | } 162 | NSString *albumUri = @""; 163 | if (album.URI != nil) { 164 | albumUri = album.URI; 165 | } 166 | 167 | return @{ 168 | @"name":album.name == nil ? @"" : album.name, 169 | @"uri":album.URI == nil ? @"" : album.URI 170 | }; 171 | } 172 | 173 | 174 | +(id)SPTAppRemoteContentItem:(NSObject *) item{ 175 | if(item == nil){ 176 | return [NSNull null]; 177 | } 178 | 179 | return @{ 180 | @"title":item.title, 181 | @"subtitle":item.subtitle, 182 | @"id":item.identifier, 183 | @"uri":item.URI, 184 | @"availableOffline":[NSNumber numberWithBool:item.availableOffline], 185 | @"playable":[NSNumber numberWithBool:item.playable], 186 | @"container":[NSNumber numberWithBool:item.container], 187 | @"children":[RNSpotifyRemoteConvert SPTAppRemoteContentItems:item.children] 188 | }; 189 | } 190 | 191 | +(id)SPTAppRemoteContentItems:(NSArray *) items{ 192 | if(items == nil){ 193 | return @[]; 194 | } 195 | NSPredicate* conformsPredicate =[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { 196 | return [evaluatedObject conformsToProtocol:@protocol(SPTAppRemoteContentItem)]; 197 | }]; 198 | NSArray* json = [ 199 | [items filteredArrayUsingPredicate: conformsPredicate] 200 | map:^id(id object) { 201 | return [RNSpotifyRemoteConvert SPTAppRemoteContentItem:object]; 202 | } 203 | ]; 204 | return json; 205 | } 206 | 207 | +(id)SPTSession:(SPTSession *)session{ 208 | if(session == nil || session.accessToken == nil || session.refreshToken == nil){ 209 | return [NSNull null]; 210 | } 211 | 212 | return @{ 213 | @"accessToken":session.accessToken, 214 | @"refreshToken":session.refreshToken, 215 | @"scope":[NSNumber numberWithUnsignedInteger: session.scope], 216 | @"expired":[NSNumber numberWithBool: session.expired], 217 | @"expirationDate":[self Date:session.expirationDate] 218 | }; 219 | } 220 | 221 | 222 | @end 223 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemoteError.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNSpotifyError.h 3 | // RNSpotify 4 | // 5 | // Created by Luis Finke on 2/15/18. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface RNSpotifyRemoteErrorCode : NSObject 12 | 13 | @property (readonly) NSString* name; 14 | @property (readonly) NSString* code; 15 | @property (readonly) NSString* message; 16 | @property (readonly) NSDictionary* reactObject; 17 | 18 | #define DECLARE_SPOTIFY_ERROR_CODE(errorName) \ 19 | @property (class, readonly) RNSpotifyRemoteErrorCode* errorName; 20 | 21 | DECLARE_SPOTIFY_ERROR_CODE(IsInitializing) 22 | DECLARE_SPOTIFY_ERROR_CODE(AlreadyInitialized) 23 | DECLARE_SPOTIFY_ERROR_CODE(NotInitialized) 24 | DECLARE_SPOTIFY_ERROR_CODE(NotImplemented) 25 | DECLARE_SPOTIFY_ERROR_CODE(NotLoggedIn) 26 | DECLARE_SPOTIFY_ERROR_CODE(MissingOption) 27 | DECLARE_SPOTIFY_ERROR_CODE(NullParameter) 28 | DECLARE_SPOTIFY_ERROR_CODE(ConflictingCallbacks) 29 | DECLARE_SPOTIFY_ERROR_CODE(BadResponse) 30 | DECLARE_SPOTIFY_ERROR_CODE(PlayerNotReady) 31 | DECLARE_SPOTIFY_ERROR_CODE(SessionExpired) 32 | DECLARE_SPOTIFY_ERROR_CODE(InvalidParameter) 33 | DECLARE_SPOTIFY_ERROR_CODE(SessionClosed) 34 | DECLARE_SPOTIFY_ERROR_CODE(AppRemoteDisconnected) 35 | DECLARE_SPOTIFY_ERROR_CODE(UnknownResponse) 36 | DECLARE_SPOTIFY_ERROR_CODE(SpotifyNotInstalled) 37 | 38 | #undef DECLARE_SPOTIFY_ERROR_CODE 39 | 40 | -(void)reject:(void(^)(NSString*,NSString*,NSError*))promiseRejector; 41 | 42 | @end 43 | 44 | 45 | 46 | @interface RNSpotifyRemoteError : NSObject 47 | 48 | -(id)initWithCode:(NSString*)code message:(NSString*)message; 49 | -(id)initWithCode:(NSString*)code error:(NSError*)error; 50 | -(id)initWithCodeObj:(RNSpotifyRemoteErrorCode*)code; 51 | -(id)initWithCodeObj:(RNSpotifyRemoteErrorCode*)code message:(NSString*)message; 52 | -(id)initWithNSError:(NSError*)error; 53 | 54 | +(instancetype)errorWithCode:(NSString*)code message:(NSString*)message; 55 | +(instancetype)errorWithCode:(NSString*)code error:(NSError*)error; 56 | +(instancetype)errorWithCodeObj:(RNSpotifyRemoteErrorCode*)code; 57 | +(instancetype)errorWithCodeObj:(RNSpotifyRemoteErrorCode*)code message:(NSString*)message; 58 | +(instancetype)errorWithNSError:(NSError*)error; 59 | 60 | -(void)reject:(void(^)(NSString*,NSString*,NSError*))promiseRejector; 61 | 62 | @property (readonly) NSString* code; 63 | @property (readonly) NSString* message; 64 | @property (readonly) NSDictionary* reactObject; 65 | 66 | +(RNSpotifyRemoteError*)nullParameterErrorForName:(NSString*)paramName; 67 | +(RNSpotifyRemoteError*)missingOptionErrorForName:(NSString*)optionName; 68 | +(RNSpotifyRemoteError*)httpErrorForStatusCode:(NSInteger)statusCode; 69 | +(RNSpotifyRemoteError*)httpErrorForStatusCode:(NSInteger)statusCode message:(NSString*)message; 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemoteError.m: -------------------------------------------------------------------------------- 1 | // 2 | // RNSpotifyError.m 3 | // RNSpotify 4 | // 5 | // Created by Luis Finke on 2/15/18. 6 | // Copyright © 2018. All rights reserved. 7 | // 8 | 9 | #import "RNSpotifyRemoteError.h" 10 | #import 11 | 12 | 13 | @interface RNSpotifyRemoteErrorCode() 14 | -(id)initWithName:(NSString*)name message:(NSString*)message; 15 | +(instancetype)codeWithName:(NSString*)name message:(NSString*)message; 16 | @end 17 | 18 | @implementation RNSpotifyRemoteErrorCode 19 | 20 | #define DEFINE_SPOTIFY_ERROR_CODE(errorName, messageStr) \ 21 | static RNSpotifyRemoteErrorCode* _RNSpotifyRemoteErrorCode##errorName = nil; \ 22 | +(RNSpotifyRemoteErrorCode*)errorName { \ 23 | if(_RNSpotifyRemoteErrorCode##errorName == nil) { \ 24 | _RNSpotifyRemoteErrorCode##errorName = [RNSpotifyRemoteErrorCode codeWithName:@#errorName message:messageStr]; } \ 25 | return _RNSpotifyRemoteErrorCode##errorName; } \ 26 | 27 | DEFINE_SPOTIFY_ERROR_CODE(IsInitializing, @"Spotify connection is initializing") 28 | DEFINE_SPOTIFY_ERROR_CODE(AlreadyInitialized, @"Spotify has already been initialized") 29 | DEFINE_SPOTIFY_ERROR_CODE(NotInitialized, @"Spotify has not been initialized") 30 | DEFINE_SPOTIFY_ERROR_CODE(NotImplemented, @"This feature has not been implemented") 31 | DEFINE_SPOTIFY_ERROR_CODE(NotLoggedIn, @"You are not logged in") 32 | DEFINE_SPOTIFY_ERROR_CODE(MissingOption, @"Missing required option") 33 | DEFINE_SPOTIFY_ERROR_CODE(NullParameter, @"Null parameter") 34 | DEFINE_SPOTIFY_ERROR_CODE(ConflictingCallbacks, @"You cannot call this function while it is already executing") 35 | DEFINE_SPOTIFY_ERROR_CODE(BadResponse, @"Invalid response format") 36 | DEFINE_SPOTIFY_ERROR_CODE(PlayerNotReady, @"Player is not ready") 37 | DEFINE_SPOTIFY_ERROR_CODE(SessionExpired, @"Your login session has expired") 38 | DEFINE_SPOTIFY_ERROR_CODE(InvalidParameter, @"Invalid Parameter Value") 39 | DEFINE_SPOTIFY_ERROR_CODE(SessionClosed, @"Session has been closed") 40 | DEFINE_SPOTIFY_ERROR_CODE(AppRemoteDisconnected, @"App Remote is not connected") 41 | DEFINE_SPOTIFY_ERROR_CODE(UnknownResponse, @"Spotify returned an unknown response") 42 | DEFINE_SPOTIFY_ERROR_CODE(SpotifyNotInstalled, @"Spotify does not appear to be installed. Note: You must whitelist the 'spotify' URL scheme in your info.plist.") 43 | 44 | #undef DEFINE_SPOTIFY_ERROR_CODE 45 | 46 | 47 | @synthesize name = _name; 48 | @synthesize message = _message; 49 | 50 | -(id)initWithName:(NSString*)name message:(NSString*)message 51 | { 52 | if(self = [super init]) 53 | { 54 | _name = [NSString stringWithString:name]; 55 | _message = [NSString stringWithString:message]; 56 | } 57 | return self; 58 | } 59 | 60 | +(instancetype)codeWithName:(NSString*)name message:(NSString*)message 61 | { 62 | return [[self alloc] initWithName:name message:message]; 63 | } 64 | 65 | -(NSString*)code 66 | { 67 | return [NSString stringWithFormat:@"RNSR%@", _name]; 68 | } 69 | 70 | -(NSDictionary*)reactObject 71 | { 72 | return @{ @"code":self.code, @"message":self.message }; 73 | } 74 | 75 | -(void)reject:(void(^)(NSString*,NSString*,NSError*))promiseRejector 76 | { 77 | promiseRejector(self.code, self.message, nil); 78 | } 79 | 80 | @end 81 | 82 | 83 | 84 | @interface RNSpotifyRemoteError() 85 | { 86 | NSError* _error; 87 | } 88 | +(NSString*)getRemoteSDKErrorCode:(SPTAppRemoteErrorCode)enumVal; 89 | +(NSString*)getSDKErrorCode:(SPTErrorCode)enumVal; 90 | @end 91 | 92 | @implementation RNSpotifyRemoteError 93 | 94 | @synthesize code = _code; 95 | @synthesize message = _message; 96 | 97 | -(id)initWithCode:(NSString*)code message:(NSString*)message 98 | { 99 | if(code == nil || code.length == 0) 100 | { 101 | code = @""; 102 | } 103 | 104 | if(self = [super init]) 105 | { 106 | _error = nil; 107 | _code = [NSString stringWithString:code]; 108 | _message = [NSString stringWithString:message]; 109 | } 110 | return self; 111 | } 112 | 113 | -(id)initWithCode:(NSString*)code error:(NSError*)error 114 | { 115 | if(code == nil || code.length == 0) 116 | { 117 | return [self initWithNSError:error]; 118 | } 119 | if(self = [super init]) 120 | { 121 | if(error == nil) 122 | { 123 | @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Cannot provide a nil error to RNSpotifyError" userInfo:nil]; 124 | } 125 | _error = error; 126 | _code = [NSString stringWithString:code]; 127 | _message = _error.localizedDescription; 128 | } 129 | return self; 130 | } 131 | 132 | -(id)initWithCodeObj:(RNSpotifyRemoteErrorCode*)code 133 | { 134 | return [self initWithCodeObj:code message:code.message]; 135 | } 136 | 137 | -(id)initWithCodeObj:(RNSpotifyRemoteErrorCode*)code message:(NSString*)message 138 | { 139 | if(self = [super init]) 140 | { 141 | _error = nil; 142 | _code = [NSString stringWithString:code.code]; 143 | _message = [NSString stringWithString:message]; 144 | } 145 | return self; 146 | } 147 | 148 | -(id)initWithNSError:(NSError*)error 149 | { 150 | if(self = [super init]) 151 | { 152 | if(error == nil) 153 | { 154 | @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Cannot provide a nil error to RNSpotifyError" userInfo:nil]; 155 | } 156 | _error = error; 157 | 158 | 159 | // Get the Error code based on error domain 160 | if([_error.domain isEqualToString:@"com.spotify.sdk.login"]) 161 | { 162 | _code = [self.class getSDKErrorCode:_error.code]; 163 | }else if([_error.domain isEqualToString:@"com.spotify.app-remote"]) 164 | { 165 | _code = [self.class getRemoteSDKErrorCode:_error.code]; 166 | } 167 | else if([_error.domain isEqualToString:@"com.spotify.ios-sdk.playback"]) 168 | { 169 | _code = [self.class getSDKErrorCode:_error.code]; 170 | } 171 | else 172 | { 173 | _code = [NSString stringWithFormat:@"%@:%ld", _error.domain, _error.code]; 174 | } 175 | 176 | 177 | // Errors will typically have nested underlyingErrors, unwrap them grabbing 178 | // each message along the way, concatenate them for more descriptive errors 179 | NSError * underlyingError = _error; 180 | NSMutableArray* messageParts = [NSMutableArray array]; 181 | while(underlyingError != nil ) 182 | { 183 | NSString* errMessage = [underlyingError localizedDescription]; 184 | if(errMessage != nil){ 185 | [messageParts addObject:errMessage]; 186 | } 187 | 188 | NSString* errRecovery = [underlyingError localizedRecoverySuggestion]; 189 | if(errRecovery != nil){ 190 | [messageParts addObject:errRecovery]; 191 | } 192 | 193 | underlyingError = underlyingError.userInfo[@"NSUnderlyingError"]; 194 | } 195 | 196 | if([messageParts count] > 0){ 197 | _message = [messageParts componentsJoinedByString:@"\r\n"]; 198 | }else{ 199 | _message = @"No error information"; 200 | } 201 | } 202 | return self; 203 | } 204 | 205 | +(instancetype)errorWithCode:(NSString*)code message:(NSString*)message 206 | { 207 | return [[self alloc] initWithCode:code message:message]; 208 | } 209 | 210 | +(instancetype)errorWithCode:(NSString *)code error:(NSError *)error 211 | { 212 | return [[self alloc] initWithCode:code error:error]; 213 | } 214 | 215 | +(instancetype)errorWithCodeObj:(RNSpotifyRemoteErrorCode*)code 216 | { 217 | return [[self alloc] initWithCodeObj:code]; 218 | } 219 | 220 | +(instancetype)errorWithCodeObj:(RNSpotifyRemoteErrorCode*)code message:(NSString*)message 221 | { 222 | return [[self alloc] initWithCodeObj:code message:message]; 223 | } 224 | 225 | +(instancetype)errorWithNSError:(NSError*)error 226 | { 227 | return [[self alloc] initWithNSError:error]; 228 | } 229 | 230 | -(void)reject:(void(^)(NSString*,NSString*,NSError*))promiseRejector 231 | { 232 | promiseRejector(_code, _message, _error); 233 | } 234 | 235 | -(NSDictionary*)reactObject 236 | { 237 | return @{ @"code":_code, @"message":_message }; 238 | } 239 | 240 | +(RNSpotifyRemoteError*)nullParameterErrorForName:(NSString*)paramName 241 | { 242 | return [RNSpotifyRemoteError errorWithCodeObj:RNSpotifyRemoteErrorCode.NullParameter 243 | message:[NSString stringWithFormat:@"%@ cannot be null", paramName]]; 244 | } 245 | 246 | +(RNSpotifyRemoteError*)missingOptionErrorForName:(NSString*)optionName 247 | { 248 | return [RNSpotifyRemoteError errorWithCodeObj:RNSpotifyRemoteErrorCode.MissingOption 249 | message:[NSString stringWithFormat:@"Missing required option %@", optionName]]; 250 | } 251 | 252 | +(RNSpotifyRemoteError*)httpErrorForStatusCode:(NSInteger)statusCode 253 | { 254 | if(statusCode <= 0) 255 | { 256 | return [RNSpotifyRemoteError errorWithCode:@"HTTPRequestFailed" message:@"Unable to send request"]; 257 | } 258 | return [RNSpotifyRemoteError errorWithCode:[NSString stringWithFormat:@"HTTP%ld", statusCode] 259 | message:[NSHTTPURLResponse localizedStringForStatusCode:statusCode]]; 260 | } 261 | 262 | +(RNSpotifyRemoteError*)httpErrorForStatusCode:(NSInteger)statusCode message:(NSString*)message 263 | { 264 | NSString* code = [NSString stringWithFormat:@"HTTP%ld", statusCode]; 265 | if(statusCode <= 0) 266 | { 267 | code = @"HTTPRequestFailed"; 268 | } 269 | return [RNSpotifyRemoteError errorWithCode:code message:message]; 270 | } 271 | 272 | 273 | 274 | #define SDK_ERROR_CASE(error) case error: return @#error; 275 | 276 | +(NSString*)getRemoteSDKErrorCode:(SPTAppRemoteErrorCode)enumVal 277 | { 278 | switch(enumVal) 279 | { 280 | SDK_ERROR_CASE(SPTAppRemoteUnknownError) 281 | SDK_ERROR_CASE(SPTAppRemoteRequestFailedError) 282 | SDK_ERROR_CASE(SPTAppRemoteInvalidArgumentsError) 283 | SDK_ERROR_CASE(SPTAppRemoteConnectionTerminatedError) 284 | SDK_ERROR_CASE(SPTAppRemoteBackgroundWakeupFailedError) 285 | SDK_ERROR_CASE(SPTAppRemoteConnectionAttemptFailedError) 286 | } 287 | return [NSString stringWithFormat:@"SPError:%ld", (NSInteger)enumVal]; 288 | } 289 | 290 | +(NSString*)getSDKErrorCode:(SPTErrorCode)enumVal 291 | { 292 | switch(enumVal) 293 | { 294 | SDK_ERROR_CASE(SPTUnknownErrorCode) 295 | SDK_ERROR_CASE(SPTJSONFailedErrorCode) 296 | SDK_ERROR_CASE(SPTRenewSessionFailedErrorCode) 297 | SDK_ERROR_CASE(SPTAuthorizationFailedErrorCode) 298 | } 299 | return [NSString stringWithFormat:@"SPError:%ld", (NSInteger)enumVal]; 300 | } 301 | 302 | @end 303 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemotePromise.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNSpotifyRemotePromise.h 3 | // RNSpotify 4 | // 5 | // Created by Luis Finke on 2/15/18. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "RNSpotifyRemoteError.h" 11 | 12 | @interface RNSpotifyRemotePromise<__covariant ObjectType> : NSObject 13 | 14 | -(id)initWithOnResolve:(void(^)(ObjectType result))resolver onReject:(void(^)(RNSpotifyRemoteError* error))rejector; 15 | -(id)initWithOnComplete:(void(^)(ObjectType result, RNSpotifyRemoteError* error))completion; 16 | 17 | -(void)resolve:(ObjectType)result; 18 | -(void)reject:(RNSpotifyRemoteError*)error; 19 | 20 | +(RNSpotifyRemotePromise*)onResolve:(void(^)(ObjectType result))onResolve onReject:(void(^)(RNSpotifyRemoteError* error))onReject; 21 | +(RNSpotifyRemotePromise*)onReject:(void(^)(RNSpotifyRemoteError* error))onReject onResolve:(void(^)(ObjectType result))onResolve; 22 | +(RNSpotifyRemotePromise*)onComplete:(void(^)(ObjectType result, RNSpotifyRemoteError* error))onComplete; 23 | 24 | + (NSArray*)popCompletionCallbacks:(NSMutableArray*)callbackArray; 25 | 26 | + (void)rejectCompletions:(NSMutableArray*)callbacks error:(RNSpotifyRemoteError*) error; 27 | 28 | + (void)resolveCompletions:(NSMutableArray*)callbacks result:(id) result; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemotePromise.m: -------------------------------------------------------------------------------- 1 | // 2 | // RNSpotifyRemotePromise.m 3 | // RNSpotify 4 | // 5 | // Created by Luis Finke on 2/15/18. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #import "RNSpotifyRemotePromise.h" 10 | 11 | @interface RNSpotifyRemotePromise() 12 | { 13 | BOOL _responded; 14 | void(^_resolver)(id); 15 | void(^_rejector)(RNSpotifyRemoteError*); 16 | void(^_completion)(id, RNSpotifyRemoteError*); 17 | } 18 | @end 19 | 20 | @implementation RNSpotifyRemotePromise 21 | 22 | -(id)initWithOnResolve:(void(^)(id))resolver onReject:(void(^)(RNSpotifyRemoteError*))rejector 23 | { 24 | if(self = [super init]) 25 | { 26 | _responded = NO; 27 | _resolver = resolver; 28 | _rejector = rejector; 29 | _completion = nil; 30 | } 31 | return self; 32 | } 33 | 34 | -(id)initWithOnComplete:(void(^)(id,RNSpotifyRemoteError*))completion 35 | { 36 | if(self = [super init]) 37 | { 38 | _responded = NO; 39 | _resolver = nil; 40 | _rejector = nil; 41 | _completion = completion; 42 | } 43 | return self; 44 | } 45 | 46 | -(void)resolve:(id)result 47 | { 48 | if(_responded) 49 | { 50 | @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"cannot call resolve or reject multiple times on a Completion object" userInfo:nil]; 51 | } 52 | _responded = YES; 53 | if(_resolver != nil) 54 | { 55 | _resolver(result); 56 | } 57 | if(_completion != nil) 58 | { 59 | _completion(result, nil); 60 | } 61 | } 62 | 63 | -(void)reject:(RNSpotifyRemoteError*)error 64 | { 65 | if(_responded) 66 | { 67 | @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"cannot call resolve or reject multiple times on a Completion object" userInfo:nil]; 68 | } 69 | _responded = YES; 70 | if(_rejector != nil) 71 | { 72 | _rejector(error); 73 | } 74 | if(_completion != nil) 75 | { 76 | _completion(nil, error); 77 | } 78 | } 79 | 80 | +(RNSpotifyRemotePromise*)onResolve:(void(^)(id))onResolve onReject:(void(^)(RNSpotifyRemoteError*))onReject 81 | { 82 | return [[self alloc] initWithOnResolve:onResolve onReject:onReject]; 83 | } 84 | 85 | +(RNSpotifyRemotePromise*)onReject:(void(^)(RNSpotifyRemoteError*))onReject onResolve:(void(^)(id))onResolve 86 | { 87 | return [[self alloc] initWithOnResolve:onResolve onReject:onReject]; 88 | } 89 | 90 | +(RNSpotifyRemotePromise*)onComplete:(void(^)(id,RNSpotifyRemoteError*))onComplete 91 | { 92 | return [[self alloc] initWithOnComplete:onComplete]; 93 | } 94 | 95 | + (NSArray*)popCompletionCallbacks:(NSMutableArray*)callbackArray{ 96 | NSArray* callbacks = [NSArray arrayWithArray:callbackArray]; 97 | [callbackArray removeAllObjects]; 98 | return callbacks; 99 | } 100 | 101 | + (void)rejectCompletions:(NSMutableArray*)callbacks error:(RNSpotifyRemoteError*) error{ 102 | NSArray* completions = [RNSpotifyRemotePromise popCompletionCallbacks:callbacks]; 103 | for(RNSpotifyRemotePromise* completion in completions) 104 | { 105 | [completion reject:error]; 106 | } 107 | } 108 | 109 | + (void)resolveCompletions:(NSMutableArray*)callbacks result:(id) result{ 110 | NSArray* completions = [RNSpotifyRemotePromise popCompletionCallbacks:callbacks]; 111 | for(RNSpotifyRemotePromise* completion in completions) 112 | { 113 | [completion resolve:result]; 114 | } 115 | } 116 | 117 | @end 118 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemoteSubscriptionCallback.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNSpotifySubscriptionCallback.h 3 | // RNSpotify 4 | // 5 | // Created by Colter McQuay on 2018-10-10. 6 | // Copyright © 2018. All rights reserved. 7 | // 8 | 9 | #ifndef RNSpotifySubscriptionCallback_h 10 | #define RNSpotifySubscriptionCallback_h 11 | 12 | // This just defines a callback that takes a void callback as an argument 13 | // This is so that we can pass an onSuccess callback to the subscriber 14 | #define CallBack void(^)(void(^)(void)) 15 | 16 | @interface RNSpotifyRemoteSubscriptionCallback : NSObject 17 | 18 | -(id)initWithCallbacks:(CallBack)subscriber unsubscriber:(CallBack)unsubscriber; 19 | -(void)subscribe; 20 | -(void)unSubscribe; 21 | -(void)reset; 22 | 23 | +(RNSpotifyRemoteSubscriptionCallback*)subscriber:(CallBack)subscriber unsubscriber:(CallBack)unsubscriber; 24 | 25 | +(RNSpotifyRemoteSubscriptionCallback*)unsubscriber:(CallBack)unsubscriber subscriber:(CallBack)subscriber; 26 | 27 | @end 28 | 29 | #endif /* RNSpotifySubscriptionCallback_h */ 30 | -------------------------------------------------------------------------------- /ios/RNSpotifyRemoteSubscriptionCallback.m: -------------------------------------------------------------------------------- 1 | // 2 | // RNSubscriptionCallback.m 3 | // RNSpotify 4 | // 5 | // Created by Colter McQuay on 2018-10-10. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "./RNSpotifyRemoteSubscriptionCallback.h" 11 | 12 | @interface RNSpotifyRemoteSubscriptionCallback() 13 | { 14 | BOOL _isSubscribed; 15 | void(^_subscriber)(void(^)(void)); 16 | void(^_unsubscriber)(void(^)(void)); 17 | } 18 | 19 | @end 20 | 21 | @implementation RNSpotifyRemoteSubscriptionCallback 22 | 23 | - (id)initWithCallbacks:(CallBack)subscriber unsubscriber:(CallBack)unsubscriber{ 24 | if(self = [super init]) 25 | { 26 | _isSubscribed = NO; 27 | _subscriber = subscriber; 28 | _unsubscriber = unsubscriber; 29 | } 30 | return self; 31 | } 32 | 33 | -(void)subscribe{ 34 | if(_isSubscribed == NO){ 35 | _subscriber(^{ 36 | self->_isSubscribed = YES; 37 | }); 38 | } 39 | } 40 | 41 | -(void)unSubscribe{ 42 | if(_isSubscribed == YES){ 43 | _unsubscriber(^{ 44 | self->_isSubscribed = NO; 45 | }); 46 | } 47 | } 48 | 49 | -(void)reset{ 50 | self->_isSubscribed = NO; 51 | } 52 | 53 | +(RNSpotifyRemoteSubscriptionCallback*)subscriber:(CallBack)subscriber unsubscriber:(CallBack)unsubscriber{ 54 | return [[self alloc] initWithCallbacks:subscriber unsubscriber:unsubscriber]; 55 | } 56 | 57 | +(RNSpotifyRemoteSubscriptionCallback*)unsubscriber:(CallBack)unsubscriber subscriber:(CallBack)subscriber{ 58 | return [[self alloc] initWithCallbacks:subscriber unsubscriber:unsubscriber]; 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-spotify-remote", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/cjam/react-native-spotify-remote.git" 6 | }, 7 | "bugs": { 8 | "url": "https://github.com/cjam/react-native-spotify-remote/issues" 9 | }, 10 | "version": "1.0.0-1", 11 | "description": "React Native wrapper around the Spotify Remote SDK", 12 | "author": { 13 | "name": "Colter McQuay" 14 | }, 15 | "licenses": [ 16 | { 17 | "type": "MIT", 18 | "url": "https://www.opensource.org/licenses/mit-license.php" 19 | } 20 | ], 21 | "nativePackage": true, 22 | "license": "ISC", 23 | "keywords": [ 24 | "react-native", 25 | "spotify", 26 | "music", 27 | "audio", 28 | "remote", 29 | "mobile", 30 | "player" 31 | ], 32 | "main": "dist/index", 33 | "module": "dist/index", 34 | "types": "dist/index.d.ts", 35 | "source": "dist/index", 36 | "react-native": "dist/index", 37 | "scripts": { 38 | "rebuild": "yarn build && yarn docs", 39 | "clean": "rm -rf ./dist", 40 | "build": "yarn clean && ./node_modules/.bin/tsc", 41 | "watch": "yarn build --watch", 42 | "docs": "typedoc --options typedoc.json --excludeExternals", 43 | "test": "echo \"Error: no test specified\" && exit 1", 44 | "prepublishOnly": "yarn rebuild", 45 | "prepack": "yarn submodules && yarn cleanup:ios", 46 | "cleanup:ios": "pushd ios/external/SpotifySDK/SpotifyiOS.framework; rm SpotifyiOS Headers; mv Versions/Current/* .; popd", 47 | "postpack": "yarn submodules", 48 | "submodules": "rm -rf ios/external/* && rm -rf android/external/* && git submodule update --init --recursive", 49 | "example": "concurrently -n \"server,packager\" -c \"yellow,cyan\" \"cd example-server && yarn start\" \"cd example && yarn start\"", 50 | "xcode": "open example/ios/example.xcworkspace", 51 | "release": "release-it", 52 | "contribute": "all-contributors add && all-contributors generate" 53 | }, 54 | "peerDependencies": { 55 | "react-native": ">=0.60" 56 | }, 57 | "dependencies": {}, 58 | "devDependencies": { 59 | "@release-it/keep-a-changelog": "^2.2.2", 60 | "@types/react-native": "^0.60.0", 61 | "all-contributors-cli": "^6.14.2", 62 | "concurrently": "^5.0.2", 63 | "release-it": "^14.6.1", 64 | "typedoc": "^0.20.24", 65 | "typescript": "^4.1.3" 66 | }, 67 | "release-it": { 68 | "hooks": { 69 | "after:bump": [ 70 | "npm run rebuild" 71 | ] 72 | }, 73 | "git": { 74 | "commitMessage": "chore: release ${version}", 75 | "tagName": "v${version}" 76 | }, 77 | "npm": { 78 | "publish": true 79 | }, 80 | "github": { 81 | "release": true 82 | }, 83 | "plugins": { 84 | "@release-it/keep-a-changelog": { 85 | "addUnreleased": true 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /react-native.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dependency: { 3 | platforms: { 4 | android: { 5 | packageImportPath: "import com.reactlibrary.RNSpotifyRemotePackage;", 6 | }, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/Album.ts: -------------------------------------------------------------------------------- 1 | export default interface Album{ 2 | 3 | /** 4 | * The name of the album. 5 | * 6 | * @type {string} 7 | * @memberof Album 8 | */ 9 | name:string; 10 | 11 | /** 12 | * The URI of the album. 13 | * 14 | * @type {string} 15 | * @memberof Album 16 | */ 17 | uri:string; 18 | } -------------------------------------------------------------------------------- /src/ApiConfig.ts: -------------------------------------------------------------------------------- 1 | import ApiScope from './ApiScope'; 2 | 3 | export default interface SpotifyApiConfig { 4 | /** 5 | * Client Id of application registered with Spotify Api 6 | * see https://developer.spotify.com/dashboard/applications 7 | * 8 | * @type {string} 9 | * @memberof SpotifyApiConfig 10 | */ 11 | clientID: string; 12 | 13 | /** 14 | * The redirect url back to your application (i.e. myapp://spotify-login-callback ) 15 | * 16 | * @type {string} 17 | * @memberof SpotifyApiConfig 18 | */ 19 | redirectURL: string; 20 | 21 | /** 22 | * Endpoint on your server to do token swap 23 | * 24 | * @type {string} 25 | * @memberof SpotifyApiConfig 26 | */ 27 | tokenSwapURL?: string; 28 | 29 | /** 30 | * Endpoint on your server to refesh token 31 | * 32 | * @type {string} 33 | * @memberof SpotifyApiConfig 34 | */ 35 | tokenRefreshURL?: string; 36 | 37 | /** 38 | * URI of Spotify item to play upon authorization. `""` will 39 | * attempt to resume playback from where it was. 40 | * 41 | * **Note:** 42 | * *If Spotify is already open and playing, this parameter will not* 43 | * *have any effect* 44 | * @type {string} 45 | * @memberof SpotifyApiConfig 46 | */ 47 | playURI?: string; 48 | 49 | /** 50 | * Requested API Scopes, need to have AppRemoteControlScope 51 | * to control playback of app 52 | * @type {ApiScope} 53 | * @memberof SpotifyApiConfig 54 | */ 55 | scopes?: ApiScope[]; 56 | 57 | /** 58 | * Whether or not the auth dialog should be shown. 59 | * Useful for debugging auth flows. 60 | * 61 | * @type {boolean} 62 | * @memberof SpotifyApiConfig 63 | */ 64 | showDialog?: boolean; 65 | 66 | /** 67 | * (Android) Choose the response type CODE or TOKEN. 68 | * Useful to determine if you need a on time login or a longterm login. 69 | * 70 | * @type {'TOKEN' | 'CODE'} 71 | * @memberof SpotifyApiConfig 72 | */ 73 | authType?: 'TOKEN' | 'CODE'; 74 | } 75 | 76 | export const API_CONFIG_DEFAULTS: Partial = { 77 | showDialog: false, 78 | scopes: [], 79 | }; 80 | -------------------------------------------------------------------------------- /src/ApiScope.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | * The Spotify Api Scopes as defined in 5 | * > iOS: https://spotify.github.io/ios-sdk/html/Constants/SPTScope.html 6 | * 7 | * > android: https://developer.spotify.com/documentation/general/guides/scopes 8 | * @enum {number} 9 | */ 10 | enum ApiScope { 11 | /** 12 | * Read access to user’s private playlists. 13 | */ 14 | PlaylistReadPrivateScope = "playlist-read-private", 15 | /** 16 | * Include collaborative playlists when requesting a user’s playlists. 17 | */ 18 | PlaylistReadCollaborativeScope = "playlist-read-collaborative", 19 | /** 20 | * Write access to a user’s public playlists. 21 | */ 22 | PlaylistModifyPublicScope = "playlist-modify-public", 23 | /** 24 | * Write access to a user’s private playlists. 25 | */ 26 | PlaylistModifyPrivateScope = "playlist-modify-private", 27 | /** 28 | * Read access to the list of artists and other users that the user follows. 29 | */ 30 | UserFollowReadScope = "user-follow-read", 31 | /** 32 | * Write/delete access to the list of artists and other users that the user follows. 33 | */ 34 | UserFollowModifyScope = "user-follow-modify", 35 | /** 36 | * Read access to a user’s “Your Music” library. 37 | */ 38 | UserLibraryReadScope = "user-library-read", 39 | /** 40 | * Write/delete access to a user’s “Your Music” library. 41 | */ 42 | UserLibraryModifyScope = "user-library-modify", 43 | /** 44 | * Read access to the user’s birthdate. (iOS) 45 | */ 46 | UserReadBirthDateScope = "user-read-birth-date", 47 | /** 48 | * Read access to user’s email address. 49 | */ 50 | UserReadEmailScope = "user-read-email", 51 | /** 52 | * Read access to user’s subscription details (type of user account). 53 | */ 54 | UserReadPrivateScope = "user-read-private", 55 | /** 56 | * Read access to a user’s top artists and tracks. 57 | */ 58 | UserTopReadScope = "user-top-read", 59 | /** 60 | * Upload user generated content images 61 | */ 62 | UGCImageUploadScope = "ugc-image-upload", 63 | /** 64 | * Control playback of a Spotify track. 65 | */ 66 | StreamingScope = "streaming", 67 | /** 68 | * Use App Remote to control playback in the Spotify app 69 | */ 70 | AppRemoteControlScope = "app-remote-control", 71 | /** 72 | * Read access to a user’s player state. 73 | */ 74 | UserReadPlaybackStateScope = "user-read-playback-state", 75 | /** 76 | * Read access to a user’s playback position. (android) 77 | */ 78 | UserReadPlaybackPosition = "user-read-playback-position", 79 | /** 80 | * Write access to a user’s playback state 81 | */ 82 | UserModifyPlaybackStateScope = "user-modify-playback-state", 83 | /** 84 | * Read access to a user’s currently playing track 85 | */ 86 | UserReadCurrentlyPlayingScope = "user-read-currently-playing", 87 | /** 88 | * Read access to a user’s currently playing track 89 | */ 90 | UserReadRecentlyPlayedScope = "user-read-recently-played", 91 | /** 92 | * Read access to a user’s currently playing track 93 | */ 94 | UserReadCurrentlyPlaying = "user-read-currently-playing", 95 | } 96 | 97 | 98 | // in iOS the scopes are flag structures, so this is used to convert from the web/android string style 99 | // as defined here: https://spotify.github.io/ios-sdk/html/Constants/SPTScope.html 100 | const ApiScopeFlagValues: { [key: string]: number } = { 101 | [ApiScope.PlaylistReadPrivateScope]: 1 << 0, 102 | [ApiScope.PlaylistReadCollaborativeScope]: 1 << 1, 103 | [ApiScope.PlaylistModifyPublicScope]: 1 << 2, 104 | [ApiScope.PlaylistModifyPrivateScope]: 1 << 3, 105 | [ApiScope.UserFollowReadScope]: 1 << 4, 106 | [ApiScope.UserFollowModifyScope]: 1 << 5, 107 | [ApiScope.UserLibraryReadScope]: 1 << 6, 108 | [ApiScope.UserLibraryModifyScope]: 1 << 7, 109 | [ApiScope.UserReadBirthDateScope]: 1 << 8, 110 | [ApiScope.UserReadEmailScope]: 1 << 9, 111 | [ApiScope.UserReadPrivateScope]: 1 << 10, 112 | [ApiScope.UserTopReadScope]: 1 << 11, 113 | [ApiScope.UGCImageUploadScope]: 1 << 12, 114 | [ApiScope.StreamingScope]: 1 << 13, 115 | [ApiScope.AppRemoteControlScope]: 1 << 14, 116 | [ApiScope.UserReadPlaybackStateScope]: 1 << 15, 117 | [ApiScope.UserModifyPlaybackStateScope]: 1 << 16, 118 | [ApiScope.UserReadCurrentlyPlayingScope]: 1 << 17, 119 | [ApiScope.UserReadRecentlyPlayedScope]: 1 << 18, 120 | } 121 | 122 | 123 | /** 124 | * Internal method used for converting array of ApiScopes to iOS bit flag based value 125 | * 126 | * @export 127 | * @param {ApiScope[]} scopes 128 | * @returns {number} 129 | */ 130 | export function getiOSScopeFromScopes(scopes: ApiScope[] = []): number { 131 | // For each scope in the array, map to its flag value and then 132 | // OR them into a single result number 133 | return scopes.map((scopeString) => ApiScopeFlagValues[scopeString] || 0) 134 | .reduce((result, current) => { 135 | return result | current; 136 | }, 0) 137 | } 138 | 139 | export default ApiScope; -------------------------------------------------------------------------------- /src/Artist.ts: -------------------------------------------------------------------------------- 1 | 2 | export default interface Artist{ 3 | /** 4 | * The name of the artist. 5 | * 6 | * @type {string} 7 | * @memberof Artist 8 | */ 9 | name:string; 10 | 11 | /** 12 | * The URI of the artist. 13 | * 14 | * @type {string} 15 | * @memberof Artist 16 | */ 17 | uri:string; 18 | } -------------------------------------------------------------------------------- /src/ContentItem.ts: -------------------------------------------------------------------------------- 1 | export default interface ContentItem{ 2 | /** 3 | * The primary title of the item. 4 | * 5 | * @type {string} 6 | * @memberof SpotifyContentItem 7 | */ 8 | title:string; 9 | /** 10 | * The secondary title of the item. 11 | * 12 | * @type {string} 13 | * @memberof SpotifyContentItem 14 | */ 15 | subtitle:string; 16 | /** 17 | * The unique identifier of the item. 18 | * 19 | * @type {string} 20 | * @memberof SpotifyContentItem 21 | */ 22 | id:string; 23 | /** 24 | * The playback URI of this item. 25 | * 26 | * @type {string} 27 | * @memberof SpotifyContentItem 28 | */ 29 | uri:string; 30 | 31 | /** 32 | * true if the item is available offline, or if it has any child that is available offline, otherwise false. 33 | * 34 | * @type {boolean} 35 | * @memberof SpotifyContentItem 36 | */ 37 | availableOffline:boolean; 38 | 39 | /** 40 | * Returns true if the item is directly playable, otherwise false. 41 | * 42 | * @type {boolean} 43 | * @memberof SpotifyContentItem 44 | */ 45 | playable:boolean; 46 | 47 | /** 48 | * Returns true if the item is expected to contain children, otherwise false. 49 | * 50 | * @type {boolean} 51 | * @memberof SpotifyContentItem 52 | */ 53 | container:boolean; 54 | 55 | /** 56 | * A list of the content item’s children. 57 | * 58 | * *Note: This is not populated for all container items as some of them are fetched lazily.* 59 | * 60 | * @type {ContentItem[]} 61 | * @memberof ContentItem 62 | */ 63 | children:ContentItem[] 64 | } -------------------------------------------------------------------------------- /src/ContentType.ts: -------------------------------------------------------------------------------- 1 | 2 | type ContentType = "default" | "fitness" | "navigation"; 3 | 4 | export default ContentType; -------------------------------------------------------------------------------- /src/CrossfadeState.ts: -------------------------------------------------------------------------------- 1 | export default interface CrossfadeState { 2 | /** 3 | * The on/off state of crossfade. 4 | * 5 | * @type {boolean} 6 | * @memberof CrossfadeState 7 | */ 8 | enabled: boolean; 9 | 10 | /** 11 | * The duration of crossfade in milliseconds. The value is meaningless if crossfade is not enabled. 12 | * 13 | * @type {number} 14 | * @memberof CrossfadeState 15 | */ 16 | duration: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/GetChildrenItemsOptions.ts: -------------------------------------------------------------------------------- 1 | import ContentType from "./ContentType"; 2 | 3 | /** 4 | * Options used when retrieving children items 5 | * 6 | * @export 7 | * @interface GetChildrenItemsOptions 8 | */ 9 | export default interface GetChildrenItemsOptions { 10 | /** 11 | * Number of items to return per page 12 | * **Android Only** 13 | * @type {number} 14 | * @memberof GetChildrenItemsOptions 15 | */ 16 | perPage?: number; 17 | 18 | /** 19 | * page offset for children items 20 | * **Android Only** 21 | * @type {number} 22 | * @memberof GetChildrenItemsOptions 23 | */ 24 | offset?: number; 25 | } 26 | 27 | 28 | export const DEFAULT_GET_CHILDREN_OPTIONS: GetChildrenItemsOptions = { 29 | perPage: 20, 30 | offset: 0 31 | } -------------------------------------------------------------------------------- /src/PlaybackOptions.ts: -------------------------------------------------------------------------------- 1 | import RepeatMode from './RepeatMode'; 2 | 3 | 4 | /** 5 | * Player Playback options 6 | * 7 | * @export 8 | * @interface PlaybackOptions 9 | */ 10 | export default interface PlaybackOptions { 11 | 12 | /** 13 | * Shuffling on / off 14 | * 15 | * @type {boolean} 16 | * @memberof PlaybackOptions 17 | */ 18 | isShuffling: boolean; 19 | 20 | /** 21 | * The repeat mode of playback 22 | * 23 | * @type {RepeatMode} 24 | * @memberof PlaybackOptions 25 | */ 26 | repeatMode: RepeatMode; 27 | } -------------------------------------------------------------------------------- /src/PlaybackRestrictions.ts: -------------------------------------------------------------------------------- 1 | export default interface PlaybackRestrictions { 2 | canSkipNext: boolean; 3 | canSkipPrevious: boolean; 4 | canRepeatTrack: boolean; 5 | canRepeatContext: boolean; 6 | canToggleShuffle: boolean; 7 | canSeek: boolean; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/PlayerContext.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * The Spotify player context 4 | * 5 | * @export 6 | * @interface PlayerContext 7 | */ 8 | export default interface PlayerContext { 9 | 10 | /** 11 | * The title of the Context 12 | * 13 | * @type {string} 14 | * @memberof PlayerContext 15 | */ 16 | title: string; 17 | 18 | /** 19 | * The uri of the Context 20 | * 21 | * @type {string} 22 | * @memberof PlayerContext 23 | */ 24 | uri: string; 25 | } 26 | -------------------------------------------------------------------------------- /src/PlayerState.ts: -------------------------------------------------------------------------------- 1 | import PlaybackOptions from './PlaybackOptions'; 2 | import PlaybackRestrictions from './PlaybackRestrictions'; 3 | import Track from './Track'; 4 | 5 | 6 | /** 7 | * The state of the player in Spotify 8 | * 9 | * @export 10 | * @interface PlayerState 11 | */ 12 | export default interface PlayerState { 13 | 14 | /** 15 | * The Current Track 16 | * 17 | * @type {Track} 18 | * @memberof PlayerState 19 | */ 20 | track: Track; 21 | 22 | /** 23 | * Current playback position in ms 24 | * 25 | * @type {number} 26 | * @memberof PlayerState 27 | */ 28 | playbackPosition: number; 29 | 30 | /** 31 | * Playback speed (podcasts) 32 | * 33 | * @type {*} 34 | * @memberof PlayerState 35 | */ 36 | playbackSpeed: any; 37 | 38 | /** 39 | * Whether the player is Paused 40 | * 41 | * @type {boolean} 42 | * @memberof PlayerState 43 | */ 44 | isPaused: boolean; 45 | 46 | /** 47 | * Any playback restrictions for the current user account 48 | * 49 | * @type {PlaybackRestrictions} 50 | * @memberof PlayerState 51 | */ 52 | playbackRestrictions: PlaybackRestrictions; 53 | 54 | 55 | /** 56 | * Current Playback options 57 | * 58 | * @type {PlaybackOptions} 59 | * @memberof PlayerState 60 | */ 61 | playbackOptions: PlaybackOptions; 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/RecommendedContentOptions.ts: -------------------------------------------------------------------------------- 1 | import ContentType from "./ContentType"; 2 | 3 | export default interface RecommendedContentOptions { 4 | /** 5 | * The type of RecomendedItems to fetch. Can be *default*, *fitness*, *navigation* 6 | * 7 | * @type {string} 8 | * @memberof RecommendedContentOptions 9 | */ 10 | type?: ContentType; 11 | 12 | /** 13 | * Flattens the returned content item containers into a single list 14 | * 15 | * @type {boolean} 16 | * @memberof RecommendedContentOptions 17 | */ 18 | flatten?: boolean; 19 | } 20 | -------------------------------------------------------------------------------- /src/RepeatMode.ts: -------------------------------------------------------------------------------- 1 | enum RepeatMode { 2 | /** 3 | * Repeat is off. 4 | */ 5 | Off=0, 6 | /** 7 | * Repeats the current track. 8 | */ 9 | Track=1, 10 | /** 11 | * Repeats the current context. 12 | */ 13 | Context=2 14 | } 15 | 16 | export default RepeatMode; -------------------------------------------------------------------------------- /src/SpotifyAuth.ts: -------------------------------------------------------------------------------- 1 | import { NativeModules, Platform } from 'react-native'; 2 | import SpotifyApiConfig, { API_CONFIG_DEFAULTS } from './ApiConfig'; 3 | import SpotifySession from './SpotifySession'; 4 | import { getiOSScopeFromScopes } from './ApiScope'; 5 | 6 | /** 7 | * Spotify Authorization Module 8 | * 9 | * *Used for managing Spotify Session* 10 | * 11 | * ```typescript 12 | * import { auth as SpotifyAuth, remote as SpotifyRemote, ApiScope, ApiConfig } from 'react-native-spotify-remote'; 13 | * const spotifyConfig: ApiConfig = { 14 | * clientID: "SPOTIFY_CLIENT_ID", 15 | * redirectURL: "SPOTIFY_REDIRECT_URL", 16 | * tokenRefreshURL: "SPOTIFY_TOKEN_REFRESH_URL", 17 | * tokenSwapURL: "SPOTIFY_TOKEN_SWAP_URL", 18 | * scope: ApiScope.AppRemoteControlScope | ApiScope.UserFollowReadScope 19 | * } 20 | * async function playEpicSong(){ 21 | * try{ 22 | * const token = await SpotifyAuth.initialize(spotifyConfig); 23 | * await SpotifyRemote.connect(token); 24 | * SpotifyRemote.playUri("spotify:track:6IA8E2Q5ttcpbuahIejO74#0:38"); 25 | * }catch(err){ 26 | * console.error("Couldn't authorize with or connect to Spotify",err); 27 | * } 28 | * } 29 | * ``` 30 | */ 31 | export interface SpotifyAuth { 32 | 33 | /** 34 | * Initializes a Session with Spotify and returns an accessToken 35 | * that can be used for interacting with other services 36 | * 37 | * @param {SpotifyApiConfig} config 38 | * @returns {Promise} accessToken 39 | * @deprecated Use `authorize` instead, will be removed in future release 40 | */ 41 | initialize(config: SpotifyApiConfig): Promise; 42 | 43 | /** 44 | * Authorizes with Spotify returning a SpotifySession object if successful 45 | * 46 | * @param {SpotifyApiConfig} config 47 | * @returns {Promise} 48 | * @memberof SpotifyAuth 49 | */ 50 | authorize(config: SpotifyApiConfig): Promise; 51 | 52 | /** 53 | * Ends the current Session and cleans up any resources 54 | * 55 | * @returns {Promise} 56 | * @memberof SpotifyAuth 57 | */ 58 | endSession(): Promise; 59 | 60 | /** 61 | * Returns the current session or `undefined` if a session hasn't been started 62 | * 63 | * @returns {Promise} 64 | * @memberof SpotifyAuth 65 | */ 66 | getSession(): Promise; 67 | } 68 | 69 | const SpotifyAuth = NativeModules.RNSpotifyRemoteAuth as SpotifyAuth; 70 | 71 | // Augment the iOS implementation of authorize to convert the Android style scopes 72 | // to flags 73 | if (Platform.OS === "ios") { 74 | const iosAuthorize = NativeModules.RNSpotifyRemoteAuth.authorize; 75 | SpotifyAuth.authorize = (config: SpotifyApiConfig) => { 76 | const iosConfig = { 77 | ...API_CONFIG_DEFAULTS, 78 | ...config, 79 | scopes: getiOSScopeFromScopes(config.scopes) 80 | } 81 | return iosAuthorize(iosConfig); 82 | } 83 | } 84 | 85 | if(Platform.OS === "android"){ 86 | const androidAuthorize = NativeModules.RNSpotifyRemoteAuth.authorize; 87 | SpotifyAuth.authorize = (config: SpotifyApiConfig) => { 88 | const mergedConfig = { 89 | ...API_CONFIG_DEFAULTS, 90 | ...config, 91 | } 92 | return androidAuthorize(mergedConfig); 93 | } 94 | } 95 | 96 | 97 | // todo: remove in future release 98 | // Here for backwards compatability 99 | SpotifyAuth.initialize = async (config: SpotifyApiConfig) => { 100 | const session = await SpotifyAuth.authorize(config); 101 | return session.accessToken; 102 | } 103 | 104 | 105 | 106 | export default SpotifyAuth; -------------------------------------------------------------------------------- /src/SpotifySession.ts: -------------------------------------------------------------------------------- 1 | import ApiScope from "./ApiScope"; 2 | 3 | export default interface SpotifySession { 4 | 5 | /** 6 | * The access token of the authenticated user. 7 | * > On Android this will be either the access token or the code depending on the `SpotifyApiConfig.AuthType` 8 | * 9 | * @type {string} 10 | * @memberof SpotifySession 11 | */ 12 | accessToken: string; 13 | 14 | /** 15 | * The refresh token. 16 | * 17 | * @type {string} 18 | * @memberof SpotifySession 19 | */ 20 | refreshToken: string; 21 | 22 | /** 23 | * The expiration date of the access token. 24 | * 25 | * @type {Date} 26 | * @memberof SpotifySession 27 | */ 28 | expirationDate: string; 29 | 30 | /** 31 | * The scope granted. **(iOS Only)** 32 | * 33 | * @type {ApiScope} 34 | * @memberof SpotifySession 35 | */ 36 | scope: ApiScope; 37 | 38 | /** 39 | * Check whether the session has expired. YES if expired; NO otherwise. 40 | * 41 | * > Note: The session is considered expired once the current date and time is equal to or greater than the expiration date and time. 42 | * 43 | * @type {boolean} 44 | * @memberof SpotifySession 45 | */ 46 | expired: boolean; 47 | } -------------------------------------------------------------------------------- /src/Track.ts: -------------------------------------------------------------------------------- 1 | import Artist from './Artist'; 2 | import Album from './Album'; 3 | 4 | export default interface Track{ 5 | name:string; 6 | uri:string; 7 | duration:number; 8 | artist:Artist; 9 | album:Album; 10 | saved:boolean; 11 | episode:boolean; 12 | podcast:boolean; 13 | 14 | // Android 15 | // ImageUri 16 | // artists 17 | 18 | // IOS 19 | // ImageProtocol: https://spotify.github.io/ios-sdk/html/Protocols/SPTAppRemoteImageAPI.html 20 | // saved: boolean 21 | } -------------------------------------------------------------------------------- /src/TypedEventEmitter.ts: -------------------------------------------------------------------------------- 1 | import { EmitterSubscription } from "react-native"; 2 | 3 | /** 4 | * An EventEmitter that supports events given by the interface in {T} 5 | * Where each Key represents the eventName and the type represents the payload 6 | * 7 | * @export 8 | * @interface TypedEventEmitter 9 | * @template T 10 | */ 11 | export default interface TypedEventEmitter { 12 | addListener(event: K, listener: (v: T[K]) => void): EmitterSubscription; 13 | /** 14 | * @deprecated Use `remove` on the EventSubscription from `addListener`. 15 | */ 16 | removeListener(event: K, listener: (v: T[K]) => void): void; 17 | removeAllListeners(event?: K): void; 18 | emit(event: K, args: T[K]): void; 19 | eventNames(): Array; 20 | listenerCount(type: K): number; 21 | /** 22 | * @deprecated Use `addListener` instead. 23 | */ 24 | on(name: K, listener: (v: T[K]) => void): EmitterSubscription; 25 | /** 26 | * @deprecated Use `remove` on the EventSubscription from `addListener`. 27 | */ 28 | off(event: K, listener: (v: T[K]) => void): void; 29 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Types 2 | export { default as ApiConfig } from './ApiConfig' 3 | export { default as ApiScope } from './ApiScope'; 4 | export { default as RepeatMode } from './RepeatMode' 5 | export { default as PlayerState } from './PlayerState'; 6 | export { default as PlayerContext } from './PlayerContext'; 7 | export { default as Track } from './Track'; 8 | export { default as Artist } from './Artist'; 9 | export { default as Album } from './Album'; 10 | export { default as ContentType } from './ContentType'; 11 | export { default as ContentItem } from './ContentItem'; 12 | export { default as CrossfadeState } from './CrossfadeState'; 13 | export { default as SpotifySession } from './SpotifySession'; 14 | export { default as GetChildrenItemsOptions } from './GetChildrenItemsOptions'; 15 | export { default as PlaybackOptions } from './PlaybackOptions'; 16 | export { default as PlaybackRestrictions } from "./PlaybackRestrictions"; 17 | export { default as RecommendedContentOptions } from "./RecommendedContentOptions"; 18 | 19 | export { SpotifyAuth } from './SpotifyAuth'; 20 | export { SpotifyRemoteApi, SpotifyRemoteEvents } from './SpotifyRemote'; 21 | 22 | // Modules 23 | import { default as _auth } from './SpotifyAuth'; 24 | import { default as _remote } from './SpotifyRemote'; 25 | 26 | /** 27 | * Singleton Instance of [[SpotifyAuth]] 28 | * ```typescript 29 | * import {auth} from 'react-native-spotify-remote' 30 | * ``` 31 | */ 32 | export const auth = _auth; 33 | 34 | /** 35 | * Singleton Instance of [[SpotifyRemoteApi]] 36 | * ```typescript 37 | * import {remote} from 'react-native-spotify-remote' 38 | * ``` 39 | */ 40 | export const remote = _remote; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */ 6 | "lib": [ 7 | "ES2017" 8 | ], /* Specify library files to be included in the compilation: */ 9 | "jsx": "react", 10 | "skipLibCheck": true, 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | "noResolve": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "esModuleInterop": true, 16 | "outDir": "./dist", /* Redirect output structure to the directory. */ 17 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | /* Strict Type-Checking Options */ 19 | "strict": true, /* Enable all strict type-checking options. */ 20 | /* Module Resolution Options */ 21 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 22 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 23 | /* Source Map Options */ 24 | "sourceRoot": "./src", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 25 | }, 26 | "include": [ 27 | "src/**/*" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "dist", 32 | "docs" 33 | ] 34 | } -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "out": "docs", 3 | "entryPoints": [ 4 | "./src/index.ts" 5 | ], 6 | "theme": "minimal", 7 | "gitRevision": "master" 8 | } --------------------------------------------------------------------------------