├── .gitignore ├── 01-gcm-web-push-notifications ├── .gitignore ├── README.md ├── dist │ └── .gitignore ├── images │ ├── icon@128.png │ ├── icon@144.png │ ├── icon@152.png │ └── icon@192.png ├── index.html ├── manifest.json ├── service-worker.js ├── src │ └── app.js └── sync-gateway-config.json ├── 02-gcm-webhook ├── README.md ├── articles.json ├── main.go ├── profiles.json ├── sync-gateway-config.json └── view.json ├── 03-ios-share-extension ├── Playgrounds │ ├── .gitignore │ ├── REST_GET_channels.playground │ │ ├── Contents.swift │ │ ├── Sources │ │ │ └── SupportCode.swift │ │ ├── contents.xcplayground │ │ ├── playground.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── timeline.xctimeline │ └── REST_POST_document.playground │ │ ├── Contents.swift │ │ ├── Sources │ │ └── SupportCode.swift │ │ ├── contents.xcplayground │ │ ├── playground.xcworkspace │ │ └── contents.xcworkspacedata │ │ └── timeline.xctimeline ├── README.md ├── TeamPicks │ ├── .gitignore │ ├── Share Track │ │ ├── Info.plist │ │ ├── MainInterface.storyboard │ │ ├── ShareViewController.swift │ │ └── TeamTableViewController.swift │ ├── TeamPicks.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── TeamPicks │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.xib │ │ │ └── Main.storyboard │ │ ├── Images.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── ViewController.swift │ └── TeamPicksTests │ │ ├── Info.plist │ │ └── TeamPicksTests.swift └── sync-gateway-config.json ├── 04-ios-sync-progress-indicator ├── .gitignore ├── CityExplorer │ ├── .gitignore │ ├── CityExplorer.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcuserdata │ │ │ └── jamesnocentini.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ ├── CityExplorer.xcworkspace │ │ └── contents.xcworkspacedata │ ├── CityExplorer │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.xib │ │ │ └── Main.storyboard │ │ ├── Images.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── ViewController.swift │ │ └── bridging-header.h │ ├── CityExplorerTests │ │ ├── CityExplorerTests.swift │ │ └── Info.plist │ ├── Podfile │ └── Podfile.lock ├── README.md ├── package.json ├── requestRx.js ├── sync-gateway-config.json └── sync.js ├── 05-android-recycler-view-animations ├── CityExplorer │ ├── .gitignore │ ├── CityExplorer.iml │ ├── app │ │ ├── .gitignore │ │ ├── app.iml │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── androidTest │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── couchbase │ │ │ │ └── cityexplorer │ │ │ │ └── ApplicationTest.java │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── couchbase │ │ │ │ └── cityexplorer │ │ │ │ ├── MainActivity.java │ │ │ │ ├── PlacesAdapter.java │ │ │ │ └── model │ │ │ │ └── Place.java │ │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ ├── ic_add_white_24dp.png │ │ │ └── ic_delete_white_24dp.png │ │ │ ├── drawable-mdpi │ │ │ ├── ic_add_white_24dp.png │ │ │ └── ic_delete_white_24dp.png │ │ │ ├── drawable-xhdpi │ │ │ ├── ic_add_white_24dp.png │ │ │ └── ic_delete_white_24dp.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── ic_add_white_24dp.png │ │ │ └── ic_delete_white_24dp.png │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ └── row_places.xml │ │ │ ├── menu │ │ │ └── menu_main.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-w820dp │ │ │ └── dimens.xml │ │ │ └── values │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle └── README.md ├── 06-channels-users-roles ├── README.md └── sync-gateway-config.json ├── 07-deploy-digital-ocean ├── Dockerfile └── README.md ├── 08-signup-and-login ├── .gitignore ├── README.md ├── SmartHome │ ├── .gitignore │ ├── SmartHome.iml │ ├── app │ │ ├── .gitignore │ │ ├── app.iml │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── androidTest │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── couchbase │ │ │ │ └── smarthome │ │ │ │ └── ApplicationTest.java │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── couchbase │ │ │ │ └── smarthome │ │ │ │ ├── Login.java │ │ │ │ ├── SignUp.java │ │ │ │ └── WelcomeActivity.java │ │ │ └── res │ │ │ ├── layout │ │ │ ├── activity_login.xml │ │ │ ├── activity_sign_up.xml │ │ │ └── activity_welcome.xml │ │ │ ├── menu │ │ │ ├── menu_login.xml │ │ │ ├── menu_sign_up.xml │ │ │ └── menu_welcome.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-v21 │ │ │ └── styles.xml │ │ │ ├── values-w820dp │ │ │ └── dimens.xml │ │ │ └── values │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── server.js └── sync-gateway-config.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/README.md: -------------------------------------------------------------------------------- 1 | # Couchbase by Example: GCM Push Notifications 2 | 3 | Push Notifications are great to engage your users effectively and prompt to notify them of important events. 4 | 5 | In this tutorial, you’ll learn how to use the Push API. Web Push Notifications enable websites and web apps on Android to receive Push Notifications just like Native application do. 6 | 7 | ## Refresher on GCM 8 | 9 | To send Push Notifications with the GCM service, we make a request to the GCM servers providing the message to send and the address to send it to (which application instance running on what device). 10 | 11 | The address in this case is called a registration ID. Google Cloud Messaging 3.0 introduced the concept of instance ID (a.k.a Identity for Robots). Instance ID is a new way to identify Android, iOS and Web apps and can be used for GCM but other use cases too. You can read more about them on Google’s developer site: 12 | 13 | > https://developers.google.com/instance-id/ 14 | 15 | When the user installs the application on the device, an instance ID will be automatically generated. Since we’re using instance IDs in the scope of GCM, we will refer to it as the **registration token**. 16 | 17 | With a simple call to the Web Push JavaScript API, you get back the registration token in the same way you would on Android and iOS. 18 | 19 | > http://www.w3.org/TR/push-api/ 20 | 21 | When we send a push notification to this registration token, Chrome will display the notification like so. 22 | 23 | ![][image-1] 24 | 25 | ## Architecture considerations 26 | 27 | We could imagine a simple news application to notify users when a new article is published through Push Notifications. The Push Notification is a visual clue to let the user know that new content is available but it could also be for the application itself to pull the new articles from the server: 28 | 29 | ![][image-2] 30 | 31 | The reason we’re using the Web Push API instead of a continuous pull replication with Sync Gateway to check for new articles is that it can work even when the browser is closed. Compared to a web socket or long polling, which will only be kept alive as long as the browser and web page is kept open. 32 | 33 | Create a new directory called `gcm-web-push-notifications`. 34 | 35 | ## Creating a Google API project 36 | 37 | Open the Google Developer Console and log into your account: 38 | 39 | > https://console.developers.google.com 40 | 41 | Create a new Project called `Timely News`: 42 | 43 | ![][image-3] 44 | 45 | Once the new project appears in the list, click on it and copy down the project number, you will need it throughout this tutorial. 46 | 47 | The Google Developer Console lists all the available APIs. To use a particular one, you must first activate it. Open the `APIs & auth > APIs` panel and enable the `Cloud Messaging for Android` api: 48 | 49 | ![][image-4] 50 | 51 | Finally, you will need a Server API key to test Push Notifications are working: 52 | 53 | ![][image-5] 54 | 55 | Copy down the Server API key as well. 56 | 57 | ## Get started with Browserify 58 | 59 | In this section, you will learn how to use browserify to bundle external dependencies such as PouchDB in your application. It’s a great way to get a project started quickly. In the `gcm-web-push-notifications` folder, run the npm command to install all the dependencies: 60 | 61 | $ npm install browserify watchify pouchdb pouchdb-find --save-dev 62 | 63 | Create a new file named `index.html` with the basic HTML5 template: 64 | 65 | ![][image-6] 66 | 67 | Create a new file `app.js` in the `src` folder, all of the logic for Timely News will reside in `app.js`. You will use the `watchify` command to automatically bundle JS files into a `bundle.js` output file. 68 | 69 | Create a new folder called `dist` and run the command: 70 | 71 | $ watchify ./src/app.js -o ./dist/bundle.js 72 | 73 | In `index.html`, don’t forget to add a script tag to link the `bundle.js` output file: 74 | 75 | ![][image-7] 76 | 77 | ## Push API and Service Worker 78 | 79 | There are three components to register and receive Push Notifications. First the manifest file that contains metadata used by Chrome. Second, retrieving the registration ID using the Push API once a Service Worker has been registered. Third, implementing methods in the Service Worker to handle incoming notifications. 80 | 81 | The manifest file contains information such as the GCM sender id (i.e. your project number) and icon images to display in the notification banner. In a new file `manifest.json`, add the following: 82 | 83 | { 84 | "name": "Timely News", 85 | "short_name": "TS", 86 | "icons": [{ 87 | "src": "images/icon@128.png", 88 | "sizes": "128x128" 89 | }, { 90 | "src": "images/icon@152.png", 91 | "sizes": "152x152" 92 | }, { 93 | "src": "images/icon@144.png", 94 | "sizes": "144x144" 95 | }, { 96 | "src": "images/icon@192.png", 97 | "sizes": "192x192" 98 | }], 99 | "start_url": "/index.html", 100 | "display": "standalone", 101 | "gcm_sender_id": "562982014144", 102 | "gcm_user_visible_only": true 103 | } 104 | 105 | **NOTE**: 1) Replace the `gcm_sender_id` with your project number. 2) You can find the icons here to use them in your project. 106 | 107 | In `app.js`, check for Service Worker support and prompt the user to get the permission to send Push Notifications. Notice we’re referencing a service worker file named `service-worker.js`. I’ll explain why in the next step. 108 | 109 | if ('serviceWorker' in navigator) { 110 | navigator.serviceWorker.register('./service-worker.js', {scope: './'}); 111 | navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) { 112 | serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true}) 113 | .then(function (pushSubscription) { 114 | console.log('The reg ID is :: ', pushSubscription.subscriptionId); 115 | 116 | }); 117 | }); 118 | } 119 | 120 | This will log the registration ID for this application instance to the console: 121 | 122 | ![][image-8] 123 | 124 | The Push API works with a Service Worker to handle incoming notifications even when the browser tab window is closed. Create a new file named `service-worker.js` with the following event listener to handle incoming notifications: 125 | 126 | self.addEventListener('push', function (event) { 127 | console.log('Received a push message', event); 128 | 129 | var notificationOptions = { 130 | body: 'The highlights of Google I/O 2015', 131 | icon: 'images/icon@192.png', 132 | tag: 'highlights-google-io-2015', 133 | data: null 134 | }; 135 | 136 | if (self.registration.showNotification) { 137 | self.registration.showNotification('Timely News', notificationOptions); 138 | return; 139 | } else { 140 | new Notification('Timely News', notificationOptions); 141 | } 142 | }); 143 | 144 | With the registration ID and API Key you can send a POST request to the GCM server to test it: 145 | 146 | curl --header "Authorization: key=API_KEY" \ 147 | --header Content-Type:"application/json" \ 148 | https://android.googleapis.com/gcm/send \ 149 | -d '{"registration_ids":["REGESTRATION_ID"]}' 150 | 151 | Now you know how to use Web notifications in Chrome! 152 | 153 | ![][image-9] 154 | 155 | You can read more about the specifics of the Push API in this great blog post: 156 | 157 | > https://developers.google.com/web/updates/2015/03/push-notificatons-on-the-open-web 158 | 159 | In the next section, you will use PouchDB to create a document of type `profile` to save the registration ID. With the PouchDB Find Plugin, you will check if a profile already exists locally. 160 | 161 | ## PouchDB & PouchDB Find 162 | 163 | In `app.js`, require the PouchDB package, attach it to the global scope (necessary for the PouchDB Inspector to work). 164 | 165 | var PouchDB = require('pouchdb'); 166 | 167 | // Expose PouchDB on the window object to use the 168 | // PouchDB Chrome debugger extension http://bit.ly/1L6dArH 169 | window.PouchDB = PouchDB; 170 | 171 | Create a database called timely-news: 172 | 173 | var db = new PouchDB('timely-news'); 174 | PouchDB.plugin(require('pouchdb-find')); 175 | 176 | After logging the registration ID to the console, use the PouchDB Find Plugin to check if a document of type profile already exists. If it doesn’t, create one and save it: 177 | 178 | db.createIndex({index: {fields: ['type']}}) 179 | .then(function() { 180 | db.find({ 181 | selector: {type: 'profile'} 182 | }).then(function (res) { 183 | console.log(res); 184 | if (res.docs.length == 0) { 185 | db.post({ 186 | 'type': 'profile', 187 | 'registration_ids': [pushSubscription.subscriptionId] 188 | }, function(err, res) { 189 | console.log(err, res); 190 | }); 191 | } 192 | }); 193 | }); 194 | 195 | Use the PouchDB Inspector to view the database content right in DevTools! 196 | 197 | > https://chrome.google.com/webstore/detail/pouchdb-inspector/hbhhpaojmpfimakffndmpmpndcmonkfa?hl=en 198 | 199 | ![][image-10] 200 | 201 | In the next section, you will get Sync Gateway set up with an in-memory database (called Walrus) and the GUEST mode enabled. Then, you will use a Push replication to save the Profile document to Sync Gateway. 202 | 203 | ## Sync Gateway 204 | 205 | Download Sync Gateway and unzip the file: 206 | 207 | > http://www.couchbase.com/nosql-databases/downloads#Couchbase\_Mobile 208 | 209 | You can find the Sync Gateway binary in the `bin` folder and examples of configuration files in the `examples` folder. Copy the `cors.json` file to the root of your project: 210 | 211 | $ cp ~/Downloads/couchbase-sync-gateway/examples/cors.json /path/to/proj/gcm-web-push-notifications/sync-gateway-config.json 212 | 213 | Start Sync Gateway and open the Admin Dashboard on `http://localhost:4985/_admin/` to keep an eye on synced documents. 214 | 215 | ![][image-11] 216 | 217 | In `app.js`, start a replication with the local "timely-news" database as source database and "http://localhost:4984/db" as the target. 218 | 219 | PouchDB.replicate('timely-news', 'http://localhost:4984/db', { 220 | live: true 221 | }); 222 | 223 | Go back to the Admin Dashboard and you should see the Profile document: 224 | 225 | ![][image-12] 226 | 227 | ## Conclusion 228 | 229 | Google Cloud Messaging enables us to develop once and be able to send messages to users on Android, iOS and Chrome. The Push API and Service Worker augment the Web experience and with a native look and feel on Android it’s a great time to implement Web Push Notification on your website. PouchDB and Sync Gateway can handle the data syncing of device tokens. In the next post, you will learn how to use the Sync Gateway Web Hook api to send Push Notifications when a particular change occurrs. 230 | 231 | [image-1]: http://cl.ly/image/253b2d0L171t/Screen_Shot_2015-06-12_at_10_14_09.png 232 | [image-2]: http://cl.ly/image/1v3V1l1s1a3b/Untitled%20Diagram%20(2).png 233 | [image-3]: http://i.gyazo.com/9522e9de1362af96a06b46e3c22ee1d7.gif 234 | [image-4]: http://i.gyazo.com/bd7393a8ad275b88914d2562334dc31e.gif 235 | [image-5]: http://i.gyazo.com/b08ad6be05e6da8cc485ab0f160eebfd.gif 236 | [image-6]: http://i.gyazo.com/7d52183f54135c3c9e3f8a32e63a48d7.gif 237 | [image-7]: http://i.gyazo.com/858cab1759038b84f36000603318db4f.gif 238 | [image-8]: http://cl.ly/image/1X1B3b3b0F30/Screen%20Shot%202015-06-13%20at%2016.21.28.png 239 | [image-9]: http://i.gyazo.com/5e52aeec4355e7f4e7341065077f9090.gif 240 | [image-10]: http://cl.ly/image/0Q1t0M361a1s/Screen%20Shot%202015-06-13%20at%2016.33.05.png 241 | [image-11]: http://i.gyazo.com/37acae33dce5e3e9d4c50945f6550b51.gif 242 | [image-12]: http://cl.ly/image/3L2Z2s081o1I/Screen%20Shot%202015-06-13%20at%2016.50.18.png -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/images/icon@128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/01-gcm-web-push-notifications/images/icon@128.png -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/images/icon@144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/01-gcm-web-push-notifications/images/icon@144.png -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/images/icon@152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/01-gcm-web-push-notifications/images/icon@152.png -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/images/icon@192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/01-gcm-web-push-notifications/images/icon@192.png -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Timely News", 3 | "short_name": "TS", 4 | "icons": [{ 5 | "src": "images/icon@128.png", 6 | "sizes": "128x128" 7 | }, { 8 | "src": "images/icon@152.png", 9 | "sizes": "152x152" 10 | }, { 11 | "src": "images/icon@144.png", 12 | "sizes": "144x144" 13 | }, { 14 | "src": "images/icon@192.png", 15 | "sizes": "192x192" 16 | }], 17 | "start_url": "/index.html", 18 | "display": "standalone", 19 | "gcm_sender_id": "562982014144", 20 | "gcm_user_visible_only": true 21 | } -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/service-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('push', function (event) { 2 | console.log('Received a push message', event); 3 | 4 | var notificationOptions = { 5 | body: 'The highlights of Google I/O 2015', 6 | icon: 'images/icon@192.png', 7 | tag: 'highlights-google-io-2015', 8 | data: null 9 | }; 10 | 11 | if (self.registration.showNotification) { 12 | self.registration.showNotification('Timely News', notificationOptions); 13 | return; 14 | } else { 15 | new Notification('Timely News', notificationOptions); 16 | } 17 | }); -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/src/app.js: -------------------------------------------------------------------------------- 1 | var PouchDB = require('pouchdb'); 2 | 3 | // Expose PouchDB on the window object to use the 4 | // PouchDB Chrome debugger extension http://bit.ly/1L6dArH 5 | window.PouchDB = PouchDB; 6 | 7 | 8 | var db = new PouchDB('timely-news'); 9 | PouchDB.plugin(require('pouchdb-find')); 10 | 11 | PouchDB.replicate('timely-news', 'http://localhost:4984/db', { 12 | live: true 13 | }); 14 | 15 | if ('serviceWorker' in navigator) { 16 | navigator.serviceWorker.register('./service-worker.js', {scope: './'}); 17 | navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) { 18 | serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true}) 19 | .then(function (pushSubscription) { 20 | console.log('The reg ID is :: ', pushSubscription.subscriptionId); 21 | 22 | db.createIndex({index: {fields: ['type']}}) 23 | .then(function() { 24 | db.find({ 25 | selector: {type: 'profile'} 26 | }).then(function (res) { 27 | console.log(res); 28 | if (res.docs.length == 0) { 29 | db.post({ 30 | 'type': 'profile', 31 | 'registration_ids': [pushSubscription.subscriptionId] 32 | }, function(err, res) { 33 | console.log(err, res); 34 | }); 35 | } 36 | }); 37 | }); 38 | 39 | }); 40 | }); 41 | } -------------------------------------------------------------------------------- /01-gcm-web-push-notifications/sync-gateway-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": ["*"], 3 | "CORS": { 4 | "Origin":["http://localhost:8000"], 5 | "LoginOrigin":["http://localhost:8000"], 6 | "Headers":["Content-Type"], 7 | "MaxAge": 1728000 8 | }, 9 | "databases": { 10 | "db": { 11 | "server": "walrus:", 12 | "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /02-gcm-webhook/README.md: -------------------------------------------------------------------------------- 1 | # Couchbase by Example: Sync Gateway Webhooks 2 | 3 | In the previous post, you learned how to set up Google Cloud Messaging with the Service Worker and Push API to handle notifications and used PouchDB + Sync Gateway to sync registration tokens. In this tutorial, you will focus exclusively on webhooks to dispatch the notifications to particular users. 4 | 5 | You will continue building Timely News, a news application to notify users of new articles matching their topics of interest. 6 | 7 | ## Scenarios 8 | 9 | There are different scenarios for sending a push notification: 10 | 11 | - **Group Messaging**: this concept was introduced in GCM to send notifications to up to 20 devices simultaneously. It’s very well suited for sending notifications to all devices that belong to a single user 12 | - **Up and Down**: a user updated a document and other users should be notified about it through a Push Notification 13 | 14 | ## Data Model 15 | 16 | Let’s start with the smallest document, a Profile document holding registration tokens of the user’s devices and topics of interest: 17 | 18 | { 19 | "type": "profile", 20 | "name": "Oliver", 21 | "subscription": "free", // other values "expired", "premium" 22 | "topics": ["g20", "science", "nsa", "design"], 23 | "registration_ids": ["AP91DIwQ", "AP91W9kX"] 24 | } 25 | 26 | And the Article document may have the following properties: 27 | 28 | { 29 | "type": "article", 30 | "title": "Design tools for developers", 31 | "content": "...", 32 | "topic": "design" 33 | } 34 | 35 | ## Group Messaging 36 | 37 | Imagine a scenario where a user is currently signed up on a freemium account and inputs a invite code to access the premium plan for a limited time. It would be nice to send a notification to all the user’s devices to fetch the additional content. 38 | 39 | **Brief**: Send a one-off notification to freemium users that also have an invite code to unlock other devices. 40 | 41 | Download the 1.1 release of Sync Gateway: 42 | 43 | > http://www.couchbase.com/nosql-databases/downloads#Couchbase_Mobile 44 | 45 | You will find the Sync Gateway binary in the `bin` folder and examples of configuration files in the `examples` folder. Copy the `exampleconfig.json` file to the root of your project: 46 | 47 | cp ~/Downloads/couchbase-sync-gateway/examples/exampleconfig.json /path/to/proj/sync-gateway-config.json 48 | 49 | Add three users in the configuration file: 50 | 51 | { 52 | "log": ["CRUD", "HTTP+"], 53 | "databases": { 54 | "db": { 55 | "server": "walrus:", 56 | "users": { 57 | "zack": { 58 | "password": "letmein" 59 | }, 60 | "ali": { 61 | "password": "letmein" 62 | }, 63 | "adam": { 64 | "password": "letmein" 65 | }, 66 | "GUEST": {"disabled": true} 67 | } 68 | } 69 | } 70 | } 71 | 72 | Add a web hook with the following properties in the `db` object: 73 | 74 | "event_handlers": { 75 | "document_changed": [ 76 | { 77 | "handler": "webhook", 78 | "url": "http://localhost:8000/invitecode", 79 | "filter": `function(doc) { 80 | if (doc.type == "profile" && doc.invite_code) { 81 | return true; 82 | } 83 | return false; 84 | }` 85 | } 86 | ] 87 | } 88 | 89 | Start Sync Gateway: 90 | 91 | $ ~/Downloads/couchbase-sync-gateway/bin/sync_gateway ./sync-gateway-config.json 92 | 93 | Create a new file `main.go` to handle the webhook: 94 | 95 | func main() { 96 | http.HandleFunc("/invitecode", func(w http.ResponseWriter, r *http.Request) { 97 | log.Println("ping") 98 | 99 | }) 100 | 101 | log.Fatal(http.ListenAndServe(":8000", nil)) 102 | } 103 | 104 | Start the Go server: 105 | 106 | $ go run main.go 107 | 108 | Using curl, make a POST request to `:4984/db/bulk_doc` to save 3 Profile documents simultaneously: 109 | 110 | curl -H 'Content-Type: application/json' \ 111 | -vX POST http://localhost:4985/db/_bulk_docs \ 112 | --data @profiles.json 113 | 114 | **NOTE**: To save space on the command line, the `--data` argument specifies that the request body is in `profiles.json`. 115 | 116 | Notice that only Ali’s Profile document is POSTed to the webhook endpoint: 117 | 118 | ![][image-1] 119 | 120 | In the next section, you will configure a second web hook to notify all users when a new article that matches their interest is published. 121 | 122 | ## Up and Down 123 | 124 | Add another webhook entry that filters only documents of type `article`: 125 | 126 | { 127 | "handler": "webhook", 128 | "url": "http://localhost:8000/new_article", 129 | "filter": `function(doc) { 130 | if (doc.type == "article") { 131 | return true; 132 | } 133 | return false; 134 | }` 135 | } 136 | 137 | Add another handler in your Go server: 138 | 139 | http.HandleFunc("/new_article", func(w http.ResponseWriter, r *http.Request) { 140 | log.Println("ping") 141 | }) 142 | 143 | Check that the webhook is working as expected by adding an Article document: 144 | 145 | curl -H 'Content-Type: application/json' \ 146 | -vX POST http://localhost:4985/db/_bulk_docs \ 147 | --data @articles.json 148 | 149 | In this case, you have to do a bit more work to figure out what set of users to notify. This is a good use case for using a view to index the Profile documents and emitting the topic as the key and registrations IDs as the value for every topic in the topics array. 150 | 151 | To register a view, we can use the Sync Gateway PUT `/_design/ddocname` endpoint with the view definition in the request body: 152 | 153 | curl -H 'Content-Type: application/json' \ 154 | -vX PUT http://localhost:4985/db/_design/extras \ 155 | --data @view.json 156 | 157 | Notice that the article we posted above has design in it’s topic and the only user subscribed to this topic is Adam. Consequently, if you query that view with the key "design", only one (key, value) pair should return with the topic as key and device tokens as value: 158 | 159 | curl -H 'Content-Type: application/json' \ 160 | -vX GET ':4985/db/_design/extras/_view/user_topics?key="design"' 161 | 162 | < HTTP/1.1 200 OK 163 | < Content-Length: 95 164 | < Content-Type: application/json 165 | * Server Couchbase Sync Gateway/1.1.0 is not blacklisted 166 | < Server: Couchbase Sync Gateway/1.1.0 167 | < Date: Wed, 17 Jun 2015 17:46:35 GMT 168 | < 169 | * Connection #0 to host left intact 170 | {"total_rows":1,"rows":[{"id":"4caa204e81b118cf23500f320e138aa8","key":"design","value":null}]} 171 | 172 | Now, you can edit handler in `main.go` to subsequently query the `user_topics` view with the key being the topic of the article: 173 | 174 | http.HandleFunc("/new_article", func(w http.ResponseWriter, r *http.Request) { 175 | log.Println("ping") 176 | 177 | var data map[string]interface{} 178 | body, _ := ioutil.ReadAll(r.Body) 179 | json.Unmarshal(body, &data) 180 | 181 | topic := data["topic"].(string) 182 | log.Printf("Querying user Profiles subscribed to %s", topic) 183 | 184 | var stringUrl string = fmt.Sprintf("http://localhost:4985/db/_design/extras/_view/user_topics?key=\"%s\"", topic) 185 | res, err := http.Get(stringUrl) 186 | if err != nil { 187 | fmt.Print(err) 188 | return 189 | } 190 | 191 | if res != nil { 192 | var result map[string]interface{} 193 | body, _ = ioutil.ReadAll(res.Body) 194 | json.Unmarshal(body, &result) 195 | log.Printf("Result from the user_topics query %v", result["rows"].([]interface {})) 196 | } 197 | }) 198 | 199 | Run the `bulk_doc` request again and you will see the list of device tokens to use in the logs: 200 | 201 | ![][image-2] 202 | 203 | ## Conclusion 204 | 205 | In this tutorial, you learned how to use Web Hooks in the scenario of GCM Push Notifications and used Couchbase Server Views to access additional information at Webhook Time™. 206 | 207 | [image-1]: http://i.gyazo.com/7ec3dd332f2d029af364590a4c2e3e63.gif 208 | [image-2]: http://i.gyazo.com/b8c8731e0cbdb5b11e8c35710fe3a092.gif -------------------------------------------------------------------------------- /02-gcm-webhook/articles.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": [ 3 | { 4 | "type": "article", 5 | "title": "Design tool for developers", 6 | "topic": "design" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /02-gcm-webhook/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "encoding/json" 7 | "io/ioutil" 8 | "fmt" 9 | ) 10 | 11 | func main() { 12 | 13 | http.HandleFunc("/invitecode", func(w http.ResponseWriter, r *http.Request) { 14 | log.Println("ping") 15 | 16 | var data map[string]interface{} 17 | 18 | body, _ := ioutil.ReadAll(r.Body) 19 | json.Unmarshal(body, &data) 20 | log.Printf("Send a notification to %s with device tokens %v", data["name"].(string), 21 | data["registration_ids"].([]interface {})) 22 | 23 | // send notification to all devices 24 | 25 | }) 26 | 27 | http.HandleFunc("/new_article", func(w http.ResponseWriter, r *http.Request) { 28 | log.Println("ping") 29 | 30 | var data map[string]interface{} 31 | 32 | body, _ := ioutil.ReadAll(r.Body) 33 | json.Unmarshal(body, &data) 34 | 35 | topic := data["topic"].(string) 36 | log.Printf("Querying user Profiles subscribed to %s", topic) 37 | 38 | var stringUrl string = fmt.Sprintf("http://localhost:4985/db/_design/extras/_view/user_topics?key=\"%s\"", topic) 39 | res, err := http.Get(stringUrl) 40 | 41 | if err != nil { 42 | fmt.Print(err) 43 | return 44 | } 45 | 46 | if res != nil { 47 | 48 | var result map[string]interface{} 49 | 50 | body, _ = ioutil.ReadAll(res.Body) 51 | json.Unmarshal(body, &result) 52 | 53 | log.Printf("Result from the user_topics query %v", result["rows"].([]interface {})) 54 | } 55 | 56 | }) 57 | 58 | log.Fatal(http.ListenAndServe(":8000", nil)) 59 | } -------------------------------------------------------------------------------- /02-gcm-webhook/profiles.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": [ 3 | { 4 | "type": "profile", 5 | "registration_ids": ["AP91SZs2"], 6 | "name": "zack" 7 | }, 8 | { 9 | "type": "profile", 10 | "name": "ali", 11 | "registration_ids": ["AP91IEsF"], 12 | "invite_code": true, 13 | "topics": ["hacking"] 14 | }, 15 | { 16 | "type": "profile", 17 | "name": "adam", 18 | "registration_ids": ["AP91DKsR", "AP91CNs8"], 19 | "topics": ["design", "science"] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /02-gcm-webhook/sync-gateway-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": ["CRUD", "HTTP+"], 3 | "adminInterface": "localhost:4985", 4 | "databases": { 5 | "db": { 6 | "server": "walrus:", 7 | "users": { 8 | "zack": { 9 | "password": "letmein" 10 | }, 11 | "ali": { 12 | "password": "letmein" 13 | }, 14 | "adam": { 15 | "password": "letmein" 16 | }, 17 | "GUEST": {"disabled": true} 18 | }, 19 | "event_handlers": { 20 | "document_changed": [ 21 | { 22 | "handler": "webhook", 23 | "url": "http://localhost:8000/invitecode", 24 | "filter": `function(doc) { 25 | if (doc.type == "profile" && doc.invite_code) { 26 | return true; 27 | } 28 | return false; 29 | }` 30 | }, 31 | { 32 | "handler": "webhook", 33 | "url": "http://localhost:8000/new_article", 34 | "filter": `function(doc) { 35 | if (doc.type == "article") { 36 | return true; 37 | } 38 | return false; 39 | }` 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /02-gcm-webhook/view.json: -------------------------------------------------------------------------------- 1 | { 2 | "views": { 3 | "user_topics": { 4 | "map": "function(doc, meta) { if (doc.type == 'profile') { doc.topics.forEach(function(topic) { emit(topic, doc.registration_ids) }) } }" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /03-ios-share-extension/Playgrounds/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Xcode ### 4 | build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.xcuserstate 18 | 19 | -------------------------------------------------------------------------------- /03-ios-share-extension/Playgrounds/REST_GET_channels.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import UIKit 4 | import XCPlayground 5 | 6 | // Let asynchronous code run 7 | XCPSetExecutionShouldContinueIndefinitely() 8 | 9 | var url = NSURL(string: "http://localhost:4984/db/_session")! 10 | var session = NSURLSession.sharedSession() 11 | 12 | var task = session.dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in 13 | 14 | var json: Dictionary = (NSJSONSerialization.JSONObjectWithData(data, options: .allZeros, error: nil) as? Dictionary)! 15 | 16 | if let userCtx = json["userCtx"] as? Dictionary { 17 | if let channels = userCtx["channels"] as? Dictionary { 18 | 19 | var array: [String] = [] 20 | 21 | for (item, _) in channels { 22 | if item != "!" { 23 | array.append(item) 24 | } 25 | } 26 | 27 | } 28 | } 29 | }) 30 | 31 | task.resume() 32 | -------------------------------------------------------------------------------- /03-ios-share-extension/Playgrounds/REST_GET_channels.playground/Sources/SupportCode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file (and all other Swift source files in the Sources directory of this playground) will be precompiled into a framework which is automatically made available to SG REST API.playground. 3 | // 4 | -------------------------------------------------------------------------------- /03-ios-share-extension/Playgrounds/REST_GET_channels.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /03-ios-share-extension/Playgrounds/REST_GET_channels.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /03-ios-share-extension/Playgrounds/REST_GET_channels.playground/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /03-ios-share-extension/Playgrounds/REST_POST_document.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import UIKit 4 | import XCPlayground 5 | 6 | // Let asynchronous code run 7 | XCPSetExecutionShouldContinueIndefinitely() 8 | 9 | let url = NSURL(string: "http://localhost:4984/db/")! 10 | let session = NSURLSession.sharedSession() 11 | 12 | let request = NSMutableURLRequest(URL: url) 13 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 14 | request.HTTPMethod = "POST" 15 | 16 | let json: Dictionary = ["name": "oliver"] 17 | let data = NSJSONSerialization.dataWithJSONObject(json, options: NSJSONWritingOptions.allZeros, error: nil) 18 | 19 | let uploadTask = session.uploadTaskWithRequest(request, fromData: data) { (data, response, error) -> Void in 20 | let json = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableLeaves, error: nil) as! Dictionary 21 | println(json) 22 | 23 | } 24 | 25 | uploadTask.resume() 26 | -------------------------------------------------------------------------------- /03-ios-share-extension/Playgrounds/REST_POST_document.playground/Sources/SupportCode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file (and all other Swift source files in the Sources directory of this playground) will be precompiled into a framework which is automatically made available to REST_POST_document.playground. 3 | // 4 | -------------------------------------------------------------------------------- /03-ios-share-extension/Playgrounds/REST_POST_document.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /03-ios-share-extension/Playgrounds/REST_POST_document.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /03-ios-share-extension/Playgrounds/REST_POST_document.playground/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /03-ios-share-extension/README.md: -------------------------------------------------------------------------------- 1 | # Couchbase by Example: iOS Share Extensions 2 | 3 | An app extension lets you extend custom functionality and content beyond your app and make it available to users while they’re using other apps or the system. In this post, you will learn how to use extensions with the Couchbase Lite iOS SDK and Sync Gateway. 4 | 5 | ## Extensions Recap 6 | 7 | Action extensions are all about changing content in-place and Share extensions is about moving content from the current host application to your application or web service. 8 | 9 | Action extensions act on the current content and because of that it uses the content as the user interface (for example, a Translate extension to translate text in-place would be an Action extension). 10 | 11 | In Safari, if we tap on the action bar on the top right, it brings up the share sheet with two types of extensions: 12 | 13 | ![][image-1] 14 | 15 | The Share Extensions are on the top and the Action Extensions are on the bottom. 16 | 17 | Imagine you are building a music curation platform and would like to allow curators to pick songs while browsing the internet. This is a perfect use case for a Share extension. In this tutorial, you will build this Share extension with multiple View Controllers on screen to share the beat with a particular curation team. 18 | 19 | ## Share Extension 20 | 21 | In Xcode, create a new Single View Application project called `TeamPicks`: 22 | 23 | ![][image-2] 24 | 25 | Then, select the **File \> New \> Target** menu item and select Share Extension in the new target wizard. Name your extension `Share Track`: 26 | 27 | ![][image-3] 28 | 29 | Select the Extension Target and run it, it should popup in Safari. Use the more menu item to display your extension in the Share Sheet by default: 30 | 31 | ![][image-4] 32 | 33 | Apple’s default share extension view has a text area populated with the title of the web page on which it was invoked and a preview area on the right. You will add a configuration item below to allow the user to pick the team to share the song with. Add a new property `item` of type `SLComposeSheetConfigurationItem` and change the `configureItems` method to read: 34 | 35 | override func configurationItems() -> [AnyObject]! { 36 | self.item = SLComposeSheetConfigurationItem() 37 | 38 | self.item.title = "Team" 39 | self.item.value = "None" 40 | 41 | self.item.tapHandler = { 42 | // TBA 43 | } 44 | 45 | return [self.item] 46 | } 47 | 48 | If the user taps on a cell, the tapHandler closure is invoked and that’s where you will push a new table view controller on the `SLComposeServiceViewController`, the same way you would with UINavigationController. First, open a new file `TeamTableViewController` subclassing `UITableViewController`: 49 | 50 | ![][image-5] 51 | 52 | In `ShareViewController`, add property `teamPickerVC` of type `TeamTableViewController` and push it on the nav stack in the tapHandler: 53 | 54 | self.item.tapHandler = { 55 | self.teamPickerVC = TeamTableViewController() 56 | self.pushConfigurationViewController(self.teamPickerVC) 57 | } 58 | 59 | Run the extension and navigate to and back from the configuration view controller. 60 | 61 | ![][image-6] 62 | 63 | In the next section, you will learn how to set up Sync Gateway with basic channels so that we can display them in the table view later on. 64 | 65 | ## Sync Gateway 66 | 67 | Download Sync Gateway and unzip the file: 68 | 69 | > http://www.couchbase.com/nosql-databases/downloads#Couchbase\_Mobile 70 | 71 | You can find the Sync Gateway binary in the `bin` folder and examples of configuration files in the `examples` folder. Copy the `cors.json` file to the root of your project: 72 | 73 | $ cp /Downloads/couchbase-sync-gateway/examples/admin_party.json /path/to/proj/sync-gateway-config.json 74 | 75 | Each team will be represented by a channel. The channel name will be the team name. Channels are a convenient way to tag documents and by giving users access to a set of channels, you can add fine grained access control. In this tutorial, you will use channels exclusively with the GUEST account (all unauthorised requests made to Sync Gateway). In a full featured application, you would likely give each user access to channels in the Sync Function. 76 | 77 | Change the configuration file to declare the list of channels, i.e. teams: 78 | 79 | { 80 | "log": ["HTTP+"], 81 | "databases": { 82 | "db": { 83 | "server": "walrus:", 84 | "users": { 85 | "GUEST": { 86 | "disabled": false, 87 | "admin_channels": ["pop", "rock", "house"] 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | **NOTE**: It’s also possible to create channels programmatically in the Sync Function by using the **channel** command. 95 | 96 | Start Sync Gateway and open the Admin Dashboard on `http://localhost:4985/_admin/` to monitor the channels and what documents were synced to them: 97 | 98 | ![][image-7] 99 | 100 | Now you can display the list of teams in the share extensions by simply using `NSURLSession` to retrieve the channel names from Sync Gateway. 101 | 102 | # Populating TeamPickerViewController 103 | 104 | In the `ViewDidLoad` method of `ShareViewController`, make a GET request to `http://localhost:4984/db/_session`. Use the Swift Playground attached to this project for hints on how to make that request: 105 | 106 | > ./Playgrounds/REST_GET_channels 107 | 108 | Pass the result to a `teams` property (of type `[String]`) on the View Controller and replace the required `UITableViewDataSource` methods: 109 | 110 | - `tableView:numberOfSectionsInTableView:` should return 1 111 | - `tableView:numberOfRowsInSection:` should return the number of teams 112 | - `tableView:cellForRowAtIndexPath:` should set the `textLabel`’s text property to a team name for each row: 113 | 114 | override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 115 | var cell: UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("TeamCell") as? UITableViewCell 116 | if (cell == nil) { 117 | cell = UITableViewCell(style: .Default, reuseIdentifier: "TeamCell") 118 | } 119 | 120 | cell!.textLabel!.text = teams[indexPath.item] 121 | return cell! 122 | } 123 | 124 | 125 | Run the extension and you should see the list of teams: 126 | 127 | ![][image-8] 128 | 129 | Now you will learn how to pass back the selected team to the ShareViewController using a protocol. Above the class definition of Table View in `TeamTableViewController.swift`, add a protocol: 130 | 131 | protocol TeamViewProtocol { 132 | func sendingViewController(viewController: TeamTableViewController, sentItem: String) 133 | } 134 | 135 | In TeamTableViewController, add a delegate property of type `TeamViewProtocol?` and implement `tableView:didSelectRowAtIndexPath`: 136 | 137 | override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 138 | self.delegate?.sendingViewController(self, sentItem: self.teams[indexPath.item]) 139 | } 140 | 141 | Implement the protocol in `ShareViewController`: 142 | 143 | func sendingViewController(viewController: TeamTableViewController, sentItem: String) { 144 | self.item.value = sentItem 145 | self.popConfigurationViewController() 146 | } 147 | 148 | In `tapHandler` of ShareViewController, set the delegate before pushing the View Controller on the navigation stack: 149 | 150 | self.item.tapHandler = { 151 | self.teamPickerVC = TeamTableViewController() 152 | self.teamPickerVC.delegate = self 153 | self.pushConfigurationViewController(self.teamPickerVC) 154 | } 155 | 156 | Run the app and selecting a team should now return to the ShareViewController with the configuration item value updated accordingly. 157 | 158 | In the next section, you will save the document to Sync Gateway when the `didSelectPost` method is called (i.e when clicking the Post button). 159 | 160 | # Saving the Pick document 161 | 162 | The final step is to save the document to Sync Gateway when the post button is pressed. This time, you will use `NSURLSession` to make a POST request to `http://localhost:4984/db/` with the track name and team name: 163 | 164 | override func didSelectPost() { 165 | // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. 166 | 167 | var properties = [ 168 | "text": self.contentText, 169 | "team": self.item.value 170 | ] 171 | 172 | let url = NSURL(string: "http://localhost:4984/db/")! 173 | let session = NSURLSession.sharedSession() 174 | 175 | let request = NSMutableURLRequest(URL: url) 176 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 177 | request.HTTPMethod = "POST" 178 | 179 | let data = NSJSONSerialization.dataWithJSONObject(properties, options: .allZeros, error: nil) 180 | 181 | let uploadTask = session.uploadTaskWithRequest(request, fromData: data) { (data, response, error) -> Void in 182 | 183 | // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context. 184 | self.extensionContext!.completeRequestReturningItems([], completionHandler: nil) 185 | } 186 | 187 | uploadTask.resume() 188 | } 189 | 190 | Check the results in the Admin Dashboard. The document should appear in the Documents tab: 191 | 192 | ![][image-9] 193 | 194 | **NOTE**: We could also use the [bulk docs][1] endpoint to save multiple documents in one API request. This is particularly useful in app extensions where there is limited time to perform network operations. 195 | 196 | ## Conclusion 197 | 198 | Share extensions are great for sharing data with your app and the Web. Setting up a Share Extension to fetch documents on-demand from Sync Gateway is a good way to give more context awareness to the user. You then have the choice to save the document back to Sync Gateway through the REST API or in the Couchbase Lite database also used by your iOS application, we will explore how to do that in the next post! 199 | 200 | [1]: http://developer.couchbase.com/mobile/develop/references/couchbase-lite/rest-api/database/post-bulk-docs/index.html 201 | 202 | [image-1]: http://cl.ly/image/1a3R200o2A1h/Screen_Shot_2015-06-13_at_22_27_31.png 203 | [image-2]: http://cl.ly/image/2S260n2l1W2c/Screen%20Shot%202015-06-17%20at%2010.54.10.png 204 | [image-3]: http://cl.ly/image/3N2R102X2Y3y/Screen%20Shot%202015-06-17%20at%2010.55.13.png 205 | [image-4]: http://i.gyazo.com/b11f135b2635fc65ec79321e9a953d03.gif 206 | [image-5]: http://i.gyazo.com/f22b1dab9393ddcfc5c5ddb96da47379.gif 207 | [image-6]: http://i.gyazo.com/4c079d89c4b87f6496c9c5aae3714885.gif 208 | [image-7]: http://i.gyazo.com/37acae33dce5e3e9d4c50945f6550b51.gif 209 | [image-8]: http://i.gyazo.com/7ce508653facb35c31ff0c1a15f1a26b.gif 210 | [image-9]: http://i.gyazo.com/21f2ad20fcdc608a7a2b174a07ccb1de.gif -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Xcode ### 4 | build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.xcuserstate 18 | 19 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/Share Track/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Share Track 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | com.couchbase.TeamPicks.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | NSExtension 26 | 27 | NSExtensionAttributes 28 | 29 | NSExtensionActivationRule 30 | TRUEPREDICATE 31 | 32 | NSExtensionMainStoryboard 33 | MainInterface 34 | NSExtensionPointIdentifier 35 | com.apple.share-services 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/Share Track/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/Share Track/ShareViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShareViewController.swift 3 | // Share Track 4 | // 5 | // Created by James Nocentini on 17/06/2015. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Social 11 | 12 | class ShareViewController: SLComposeServiceViewController, TeamViewProtocol { 13 | 14 | var item: SLComposeSheetConfigurationItem! 15 | var teamPickerVC: TeamTableViewController! 16 | 17 | override func isContentValid() -> Bool { 18 | // Do validation of contentText and/or NSExtensionContext attachments here 19 | return true 20 | } 21 | 22 | override func didSelectPost() { 23 | // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. 24 | 25 | var properties = [ 26 | "text": self.contentText, 27 | "team": self.item.value 28 | ] 29 | 30 | let url = NSURL(string: "http://localhost:4984/db/")! 31 | let session = NSURLSession.sharedSession() 32 | 33 | let request = NSMutableURLRequest(URL: url) 34 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 35 | request.HTTPMethod = "POST" 36 | 37 | let data = NSJSONSerialization.dataWithJSONObject(properties, options: .allZeros, error: nil) 38 | 39 | let uploadTask = session.uploadTaskWithRequest(request, fromData: data) { (data, response, error) -> Void in 40 | 41 | // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context. 42 | self.extensionContext!.completeRequestReturningItems([], completionHandler: nil) 43 | } 44 | 45 | uploadTask.resume() 46 | } 47 | 48 | override func configurationItems() -> [AnyObject]! { 49 | self.item = SLComposeSheetConfigurationItem() 50 | 51 | self.item.title = "Team" 52 | self.item.value = "None" 53 | 54 | self.item.tapHandler = { 55 | self.teamPickerVC = TeamTableViewController() 56 | self.teamPickerVC.delegate = self 57 | self.pushConfigurationViewController(self.teamPickerVC) 58 | } 59 | 60 | return [self.item] 61 | } 62 | 63 | func sendingViewController(viewController: TeamTableViewController, sentItem: String) { 64 | self.item.value = sentItem 65 | self.popConfigurationViewController() 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/Share Track/TeamTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TeamTableViewController.swift 3 | // TeamPicks 4 | // 5 | // Created by James Nocentini on 17/06/2015. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol TeamViewProtocol { 12 | func sendingViewController(viewController: TeamTableViewController, sentItem: String) 13 | } 14 | 15 | class TeamTableViewController: UITableViewController { 16 | 17 | var teams: [String] = [] 18 | var delegate: TeamViewProtocol? 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | var url = NSURL(string: "http://localhost:4984/db/_session")! 24 | var session = NSURLSession.sharedSession() 25 | 26 | var task = session.dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in 27 | 28 | var json: Dictionary = (NSJSONSerialization.JSONObjectWithData(data, options: .allZeros, error: nil) as? Dictionary)! 29 | 30 | if let userCtx = json["userCtx"] as? Dictionary { 31 | if let channels = userCtx["channels"] as? Dictionary { 32 | 33 | var array: [String] = [] 34 | 35 | for (item, _) in channels { 36 | if item != "!" { 37 | array.append(item) 38 | } 39 | } 40 | 41 | self.teams = array 42 | 43 | dispatch_async(dispatch_get_main_queue(), { () -> Void in 44 | self.tableView.reloadData() 45 | }) 46 | 47 | } 48 | } 49 | }) 50 | 51 | task.resume() 52 | 53 | 54 | 55 | self.clearsSelectionOnViewWillAppear = false 56 | } 57 | 58 | override func didReceiveMemoryWarning() { 59 | super.didReceiveMemoryWarning() 60 | // Dispose of any resources that can be recreated. 61 | } 62 | 63 | // MARK: - Table view data source 64 | 65 | override func numberOfSectionsInTableView(tableView: UITableView) -> Int { 66 | // #warning Potentially incomplete method implementation. 67 | // Return the number of sections. 68 | return 1 69 | } 70 | 71 | override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 72 | // #warning Incomplete method implementation. 73 | // Return the number of rows in the section. 74 | return self.teams.count 75 | } 76 | 77 | override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 78 | 79 | var cell = tableView.dequeueReusableCellWithIdentifier("TeamCell") as? UITableViewCell 80 | 81 | if (cell == nil) { 82 | cell = UITableViewCell(style: .Default, reuseIdentifier: "TeamCell") 83 | } 84 | 85 | cell!.textLabel!.text = self.teams[indexPath.item] 86 | 87 | return cell! 88 | } 89 | 90 | override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 91 | self.delegate?.sendingViewController(self, sentItem: self.teams[indexPath.item]) 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/TeamPicks.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/TeamPicks/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TeamPicks 4 | // 5 | // Created by James Nocentini on 17/06/2015. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/TeamPicks/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/TeamPicks/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/TeamPicks/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 | } -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/TeamPicks/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.couchbase.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/TeamPicks/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // TeamPicks 4 | // 5 | // Created by James Nocentini on 17/06/2015. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | override func didReceiveMemoryWarning() { 19 | super.didReceiveMemoryWarning() 20 | // Dispose of any resources that can be recreated. 21 | } 22 | 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/TeamPicksTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.couchbase.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /03-ios-share-extension/TeamPicks/TeamPicksTests/TeamPicksTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TeamPicksTests.swift 3 | // TeamPicksTests 4 | // 5 | // Created by James Nocentini on 17/06/2015. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class TeamPicksTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /03-ios-share-extension/sync-gateway-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": ["HTTP+"], 3 | "databases": { 4 | "db": { 5 | "server": "walrus:", 6 | "users": { 7 | "GUEST": { 8 | "disabled": false, 9 | "admin_channels": ["pop", "rock", "house"] 10 | } 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | node_modules -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Xcode ### 4 | build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.xcuserstate 18 | 19 | Pods -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorer.xcodeproj/project.xcworkspace/xcuserdata/jamesnocentini.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/04-ios-sync-progress-indicator/CityExplorer/CityExplorer.xcodeproj/project.xcworkspace/xcuserdata/jamesnocentini.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorer.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorer/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CityExplorer 4 | // 5 | // Created by James Nocentini on 21/06/2015. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(application: UIApplication) { 32 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorer/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorer/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorer/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 | } -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.couchbase.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorer/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CityExplorer 4 | // 5 | // Created by James Nocentini on 21/06/2015. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | var pull: CBLReplication? 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | // Do any additional setup after loading the view, typically from a nib. 18 | 19 | let manager = CBLManager.sharedInstance() 20 | let databaseExists = manager.databaseExistsNamed("cityexplorer") 21 | var database = manager.databaseNamed("cityexplorer", error: nil) 22 | if databaseExists { 23 | database?.deleteDatabase(nil) 24 | database = manager.databaseNamed("cityexplorer", error: nil) 25 | } 26 | 27 | let gateway = NSURL(string: "http://localhost:4984/db")! 28 | 29 | pull = database?.createPullReplication(gateway) 30 | 31 | let nctr = NSNotificationCenter.defaultCenter() 32 | nctr.addObserver(self, selector: "replicationProgress:", name: kCBLReplicationChangeNotification, object: pull) 33 | 34 | pull?.start() 35 | } 36 | 37 | @IBOutlet var progressView: UIProgressView! 38 | 39 | func replicationProgress(notification: NSNotification) { 40 | 41 | let active = pull?.status == .Active 42 | let completed = pull?.status == .Stopped 43 | 44 | println("Status : \(pull?.status.rawValue)") 45 | println("Changes Count: \(pull?.changesCount)") 46 | println("Completed Count: \(pull?.completedChangesCount)") 47 | println("======") 48 | 49 | if pull!.changesCount > 0 { 50 | let number = Float(pull!.completedChangesCount) / Float(pull!.changesCount) 51 | self.progressView.progress = number 52 | } 53 | 54 | } 55 | 56 | override func didReceiveMemoryWarning() { 57 | super.didReceiveMemoryWarning() 58 | // Dispose of any resources that can be recreated. 59 | } 60 | 61 | 62 | } 63 | 64 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorer/bridging-header.h: -------------------------------------------------------------------------------- 1 | // 2 | // bridging-header.h 3 | // CityExplorer 4 | // 5 | // Created by James Nocentini on 21/06/2015. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | #ifndef CityExplorer_bridging_header_h 10 | #define CityExplorer_bridging_header_h 11 | 12 | #include 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorerTests/CityExplorerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CityExplorerTests.swift 3 | // CityExplorerTests 4 | // 5 | // Created by James Nocentini on 21/06/2015. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class CityExplorerTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/CityExplorerTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.couchbase.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '6.0' 3 | 4 | target 'CityExplorer' do 5 | 6 | pod 'couchbase-lite-ios', '~> 1.1.0' 7 | 8 | end 9 | 10 | target 'CityExplorerTests' do 11 | 12 | end 13 | 14 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/CityExplorer/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - couchbase-lite-ios (1.1.0): 3 | - couchbase-lite-ios/Core (= 1.1.0) 4 | - couchbase-lite-ios/Core (1.1.0) 5 | 6 | DEPENDENCIES: 7 | - couchbase-lite-ios (~> 1.1.0) 8 | 9 | SPEC CHECKSUMS: 10 | couchbase-lite-ios: fc7be01032dc1a4c923d1a52b6734eb9b20bf072 11 | 12 | COCOAPODS: 0.37.1 13 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/README.md: -------------------------------------------------------------------------------- 1 | # Couchbase by Example: Displaying a Sync Progress Indicator 2 | 3 | `NSProgress` is an object in Foundation that represents the completion of some work. That work could be downloading a file, installing an app or something your own application is doing. 4 | 5 | The `NSProgress` exists to let you easily report progress in your application across various components both for the UI and the system. With Couchbase Mobile, data is exchanged by initiating replications and with those come change events that can inform you of the progress. 6 | 7 | In this tutorial, you'll import data from the Google Places API to Sync Gateway and replicate them to an iOS app. 8 | 9 | > https://developers.google.com/places/ 10 | 11 | Along the way, you'll learn how to: 12 | 13 | - Use the Sync Gateway Admin REST API to import data from an external source. 14 | - Setup a pull replication with the iOS SDK. 15 | - Use replication change notifications to display a progress bar in the UI. 16 | 17 | Let's get started! 18 | 19 | ## Getting Started 20 | 21 | To use the Google Places API in this tutorial, you will first create a new project in the Google Developer Console and then generate a Server API Key. 22 | 23 | Open the Google Developer Console and log into your account: 24 | 25 | > https://console.developers.google.com 26 | 27 | Create a new Project called **City Explorer**: 28 | 29 | ![](http://i.gyazo.com/83a703f1a7330e42f7a92f62023c4daf.gif) 30 | 31 | Once the new project appears in the list, click on it and navigate to **APIs & auth > APIs** in the left navigation drawer. Enable the **Google Places API Web Service**. 32 | 33 | Once enabled, go to the **Credentials** tab in the left navigation drawer, create a new Key (Server key) and copy down the API key as you will need it throughout this tutorial. 34 | 35 | In the next section, you will use a couple libraries and the Admin REST API to sync the Places data to Sync Gateway. 36 | 37 | ## Sync Gateway 38 | 39 | Download Sync Gateway and unzip the file: 40 | 41 | > http://www.couchbase.com/nosql-databases/downloads#Couchbase\_Mobile 42 | 43 | You can find the Sync Gateway binary in the **bin** folder and examples of configuration files in the **examples** folder. Copy the **basic-walrus-bucket.json** file to the root of your project: 44 | 45 | $ cp /Downloads/couchbase-sync-gateway/examples/basic-walrus-bucket.json /path/to/proj/sync-gateway-config.json 46 | 47 | Start Sync Gateway: 48 | 49 | $ ~/Downloads/couchbase-sync-gateway/bin/sync_gateway 50 | 51 | Open the Admin Dashboard to monitor the documents that were saved to Sync Gateway. 52 | 53 | http://localhost:4985/_admin/ 54 | 55 | In the next section, you will write a small NodeJS app with the RxJS and Request modules to import the Places data to Sync Gateway. 56 | 57 | ## Places API → Sync Gateway 58 | 59 | Before you start scripting the app server, check that your API Key is working correctly, open the following url in your browser, you should see the JSON response. 60 | 61 | https://maps.googleapis.com/maps/api/place/textsearch/json?query=restaurants+in+London&key=API_KEY 62 | 63 | **NOTE**: Don’t forget to add your API Key in the URL. 64 | 65 | ![](http://cl.ly/image/1X3A1S3x180b/sync_progress_indicator.png) 66 | 67 | To build the app server that will import the data from the Places API to Sync Gateway, you will use [RxJS](https://github.com/Reactive-Extensions/RxJS) and [Request](https://github.com/request/request). Code that deals with more than one event or asynchronous computation gets complicated quickly. RxJS makes these computations *first-class citizens* and provides a model that allows for readable and composable APIs to deal with these asynchronous computations. The Request module is the de-facto library to make http requests in NodeJS simpler than ever. Go ahead and install the dependencies: 68 | 69 | $ npm install request rx --save 70 | 71 | Copy **requestRx.js** from the [GitHub repo](https://github.com/couchbaselabs/Couchbase-by-Example/blob/master/04-ios-sync-progress-indicator/requestRx.js) to your project folder. We’re simply wrapping the Request api in RxJS constructs (flatMap, filter, subscribe...). For example, instead of using `request.get`, you will use `requestRx.get`. 72 | 73 | Create a new file called **sync.js**, require the `requestRx` and `Rx` modules. Define a couple constants: 74 | 75 | const api_key = 'AIzaSyBGRQzQ2Sy1zgIrMrbYUknd1L25idYOoII'; 76 | const url = 'https://maps.googleapis.com/maps/api/place'; 77 | const gateway = 'http://localhost:4985/db'; 78 | 79 | **NOTE**: You will use the JavaScript ES 6 syntax (more specifically string interpolation and arrow functions) which will make your program shorter and more readable. 80 | 81 | Next, use the `requestRx` method to follow the chain of requests describe in the diagram. 82 | 83 | If you are wondering how to use Reactive Extensions, I stronly encourage you to follow [this tutorial](http://reactive-extensions.github.io/learnrx/). It will take a couple hours to complete but you will come out of it with a very clear understanding of Reactive Extensions: 84 | 85 | http://reactive-extensions.github.io/learnrx/ 86 | 87 | This might be a lot to take in but the best think to do is experiment with the different operators (flatMap, zip, subscribe, fromArray): 88 | 89 | ```javascript 90 | // 1. Search for Places 91 | requestRx.get(`${url}/textsearch/json?key=${api_key}&query=restaurants+in+london`) 92 | .subscribe((res) => { 93 | var places = JSON.parse(res.body).results; 94 | var placesStream = Rx.Observable.fromArray(places); 95 | 96 | // 2. Send the Places in bulk to Sync Gateway 97 | requestRx({uri: `${gateway}/_bulk_docs`, method: 'POST', json: {docs: places}}) 98 | .flatMap((docsRes) => { 99 | var docsStream = Rx.Observable.fromArray(docsRes.body); 100 | 101 | // Merge the place's photoreference with the doc id and rev 102 | return Rx.Observable.zip(placesStream, docsStream, (place, doc) => { 103 | return { 104 | id: doc.id, 105 | rev: doc.rev, 106 | ref: place.photos[0].photo_reference 107 | } 108 | }); 109 | }) 110 | .flatMap((doc) => { 111 | 112 | // 3. Get the binary jpg photo using the ref property (i.e. photoreference) 113 | var options = { 114 | uri: `${url}/photo?key=${api_key}&maxwidth=400&photoreference=${doc.ref}`, 115 | encoding: null 116 | }; 117 | return requestRx.get(options) 118 | .flatMap((photo) => { 119 | 120 | // 4. Save the photo as an attachment on the corresponding document 121 | return requestRx({ 122 | uri: `${gateway}/${doc.id}/photo?rev=${doc.rev}`, 123 | method: 'PUT', 124 | headers: {'Content-Type': 'image/jpg'}, 125 | body: photo.body 126 | }) 127 | }) 128 | }) 129 | .subscribe((res) => { 130 | }); 131 | }); 132 | ``` 133 | 134 | 1. Get the Places that match the query `restaurants in London`. Use the ES 6 string interpolation feature in the url. 135 | 2. The `_bulk_docs` endpoint is very convenient for importing large datasets to a Sync Gateway instance. Read more about it in the [docs](http://developer.couchbase.com/mobile/develop/references/sync-gateway/rest-api/database/post-bulk-docs/index.html). 136 | 3. After saving the document, you save the photo as an attachment, you must first get the image from the Places API. Notice the `encoding` property is set to `null`. This is required by the Request module for any response body that isn’t a string. Read more about it in the [Request docs](https://github.com/request/request#user-content-requestoptions-callback). 137 | 4. You must tell Sync Gateway which document (by specifying the document id) and revision of that document (by specifying the revision number) to save this attachment on. 138 | 139 | To run your NodeJS app written with the JavaScript ES 6 syntax, you can use [Babel](https://babeljs.io/). Install it and run it with the **sync.js** file: 140 | 141 | $ npm install babel -g 142 | $ babel-node sync.js 143 | 144 | ![](http://i.gyazo.com/5ff13132ea63ec95299165ab2868f4f8.gif) 145 | 146 | Now that you have documents including images stored in the in-memory bucket of Sync Gateway, you will start coding the iOS app to include a progress bar managed by the replication change notification. 147 | 148 | ## iOS application 149 | 150 | In Xcode, create a new **Single View Application** called **CityExplorer**: 151 | 152 | ![](http://cl.ly/image/0b2x1E2I012W/Screen%20Shot%202015-06-21%20at%2017.44.52.png) 153 | 154 | Close the project and install the Couchbase Lite iOS SDK via Cocoapods: 155 | 156 | $ pod init 157 | $ pod search couchbase 158 | 159 | Add the dependency to the **Podfile** in the root of the project, then run install: 160 | 161 | $ pod install 162 | 163 | Open **CityExplorer.workspace** this time and create a bridging header: 164 | 165 | ![](http://i.gyazo.com/cc51fa04f3f3ae421bfee381e283e7b0.gif) 166 | 167 | Navigate to build settings to add the bridging header: 168 | 169 | ![](http://i.gyazo.com/2993e437948f8001a351a92b511e843a.gif) 170 | 171 | Open `ViewController.swift` and add a property `pull` of type `CBLReplication?`. In the `viewDidLoad` method, add the following: 172 | 173 | // 1 174 | let manager = CBLManager.sharedInstance() 175 | // 2 176 | let databaseExists = manager.databaseExistsNamed("cityexplorer") 177 | var database = manager.databaseNamed("cityexplorer", error: nil) 178 | if databaseExists { 179 | database?.deleteDatabase(nil) 180 | database = manager.databaseNamed("cityexplorer", error: nil) 181 | } 182 | 183 | let gateway = NSURL(string: "http://localhost:4984/db")! 184 | 185 | // 3 186 | pull = database?.createPullReplication(gateway) 187 | 188 | let nctr = NSNotificationCenter.defaultCenter() 189 | nctr.addObserver(self, selector: "replicationProgress:", name: kCBLReplicationChangeNotification, object: pull) 190 | 191 | // 4 192 | pull?.start() 193 | 194 | A couple of things are happening above: 195 | 196 | 1. You get the shared instance of the manager. 197 | 2. With the manager instance, you delete the content of the database. This will ensure that the replication starts from scratch every time you run the app. 198 | 3. You instantiate a pull replication and register as an oberserver on the notification named `kCBLReplicationChangeNotification`. 199 | 4. You start the replication. 200 | 201 | And add the `replicationProgress` method to simply log the changesCount and completedChangesCount properties: 202 | 203 | func replicationProgress(notification: NSNotification) { 204 | println("Changes count \(pull?.changesCount)") 205 | println("Completed \(pull?.completedChangesCount)") 206 | } 207 | 208 | In the next section, you will add a progress view in the Storyboard, then will connect it to the View Controller. 209 | 210 | ## Progress Bar 211 | 212 | In the Storyboard, add a Progress View in the centre of the View: 213 | 214 | ![](http://cl.ly/image/0d330e3R3r3R/Screen%20Shot%202015-06-21%20at%2019.24.17.png) 215 | 216 | Connect the UI handle to the controller property: 217 | 218 | ![](http://cl.ly/image/2S01342L2933/Screen%20Shot%202015-06-21%20at%2019.29.53.png) 219 | 220 | In the `replicationProgress` method, update the progressView's `progress` property accordingly: 221 | 222 | let active = pull?.status == .Active 223 | let completed = pull?.status == .Stopped 224 | 225 | println("Status : \(pull?.status.rawValue)") 226 | println("Changes Count: \(pull?.changesCount)") 227 | println("Completed Count: \(pull?.completedChangesCount)") 228 | println("======") 229 | 230 | if pull!.changesCount > 0 { 231 | let number = Float(pull!.completedChangesCount) / Float(pull!.changesCount) 232 | self.progressView.progress = number 233 | } 234 | 235 | Run the app and you should see the progress bar updating as the documents are replicated: 236 | 237 | ![](http://i.gyazo.com/148bfa61a0f5b8c1ba2fa3e09c5f807e.gif) 238 | 239 | You can run the **sync.js** script a couple times to have more documents to pull. The Places API returns a maximum of 20 results in one response. 240 | 241 | ## Conclusion 242 | 243 | In this tutorial, you learned how to set up a project to use the Google Places API and a NodeJS program to import data as documents and attachments in Sync Gateway. You also used the `NSNotification` api on iOS to register for the `kCBLReplicationChangeNotification` notification and update the progress view in your iOS application. -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "04-ios-sync-progress-indicator", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "requestRx.js", 6 | "dependencies": { 7 | "rx": "~2.5.3", 8 | "request": "~2.58.0" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "ISC" 16 | } 17 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/requestRx.js: -------------------------------------------------------------------------------- 1 | const rx = require('rx'); 2 | const request = require('request'); 3 | 4 | let wrapMethodInRx = (method) => { 5 | return function (...args) { 6 | return rx.Observable.create((subj) => { 7 | // Push the callback as the last parameter 8 | args.push((err, resp, body) => { 9 | if (err) { 10 | subj.onError(err); 11 | return; 12 | } 13 | 14 | if (resp.statusCode >= 400) { 15 | subj.onError(new Error(`Request failed: ${resp.statusCode}\n${body}`)); 16 | return; 17 | } 18 | 19 | subj.onNext({response: resp, body: body}); 20 | subj.onCompleted(); 21 | }); 22 | 23 | try { 24 | method(...args); 25 | } catch (e) { 26 | subj.onError(e); 27 | } 28 | 29 | return rx.Disposable.empty; 30 | }) 31 | } 32 | }; 33 | 34 | let requestRx = wrapMethodInRx(request); 35 | requestRx.get = wrapMethodInRx(request.get); 36 | requestRx.post = wrapMethodInRx(request.post); 37 | requestRx.patch = wrapMethodInRx(request.patch); 38 | requestRx.put = wrapMethodInRx(request.put); 39 | requestRx.del = wrapMethodInRx(request.del); 40 | 41 | requestRx.pipe = (url, stream) => { 42 | return rx.Observable.create((subj) => { 43 | try { 44 | request.get(url).pipe(stream) 45 | .on('error', (err) => subj.onError(err)) 46 | .on('end', () => { subj.onNext(true); subj.onCompleted(); }); 47 | } catch (e) { 48 | subj.onError(e); 49 | } 50 | }); 51 | }; 52 | 53 | module.exports = requestRx; -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/sync-gateway-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": ["*"], 3 | "databases": { 4 | "db": { 5 | "server": "walrus:", 6 | "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } } 7 | } 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /04-ios-sync-progress-indicator/sync.js: -------------------------------------------------------------------------------- 1 | var requestRx = require('./requestRx.js'); 2 | var Rx = require('rx'); 3 | 4 | const api_key = 'AIzaSyD4e6ZUIc9G2AxKansIUKa0enFzWZy5h8w'; 5 | const url = 'https://maps.googleapis.com/maps/api/place'; 6 | const gateway = 'http://localhost:4985/db'; 7 | 8 | // 1. Search for Places 9 | requestRx.get(`${url}/textsearch/json?key=${api_key}&query=restaurants+in+london`) 10 | .subscribe((res) => { 11 | var places = JSON.parse(res.body).results; 12 | var placesStream = Rx.Observable.fromArray(places); 13 | 14 | // 2. Send the Places in bulk to Sync Gateway 15 | requestRx({uri: `${gateway}/_bulk_docs`, method: 'POST', json: {docs: places}}) 16 | .flatMap((docsRes) => { 17 | var docsStream = Rx.Observable.fromArray(docsRes.body); 18 | 19 | // Merge the place's photoreference with the doc id and rev 20 | return Rx.Observable.zip(placesStream, docsStream, (place, doc) => { 21 | return { 22 | id: doc.id, 23 | rev: doc.rev, 24 | ref: place.photos[0].photo_reference 25 | } 26 | }); 27 | }) 28 | .flatMap((doc) => { 29 | 30 | // 3. Get the binary jpg photo using the ref property (i.e. photoreference) 31 | var options = { 32 | uri: `${url}/photo?key=${api_key}&maxwidth=400&photoreference=${doc.ref}`, 33 | encoding: null 34 | }; 35 | return requestRx.get(options) 36 | .flatMap((photo) => { 37 | 38 | // 4. Save the photo as an attachment on the corresponding document 39 | return requestRx({ 40 | uri: `${gateway}/${doc.id}/photo?rev=${doc.rev}`, 41 | method: 'PUT', 42 | headers: {'Content-Type': 'image/jpg'}, 43 | body: photo.body 44 | }) 45 | }) 46 | }) 47 | .subscribe((res) => { 48 | }); 49 | }); -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | # Created by https://www.gitignore.io 9 | 10 | ### Android ### 11 | # Built application files 12 | *.apk 13 | *.ap_ 14 | 15 | # Files for the Dalvik VM 16 | *.dex 17 | 18 | # Java class files 19 | *.class 20 | 21 | # Generated files 22 | bin/ 23 | gen/ 24 | 25 | # Gradle files 26 | .gradle/ 27 | build/ 28 | /*/build/ 29 | 30 | # Local configuration file (sdk path, etc) 31 | local.properties 32 | 33 | # Proguard folder generated by Eclipse 34 | proguard/ 35 | 36 | # Log Files 37 | *.log 38 | 39 | ### Android Patch ### 40 | gen-external-apklibs 41 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/CityExplorer.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.couchbase.cityexplorer" 9 | minSdkVersion 14 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | 21 | // workaround for "duplicate files during packaging of APK" issue 22 | // see https://groups.google.com/d/msg/adt-dev/bl5Rc4Szpzg/wC8cylTWuIEJ 23 | packagingOptions { 24 | exclude 'META-INF/ASL2.0' 25 | exclude 'META-INF/LICENSE' 26 | exclude 'META-INF/NOTICE' 27 | } 28 | } 29 | 30 | dependencies { 31 | compile fileTree(dir: 'libs', include: ['*.jar']) 32 | compile 'com.couchbase.lite:couchbase-lite-android:1.1.0' 33 | compile 'com.android.support:recyclerview-v7:22.2.0' 34 | compile 'com.android.support:design:22.2.0' 35 | 36 | } 37 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/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 /Users/jamesnocentini/Developer/adt-bundle/sdk/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 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/androidTest/java/com/couchbase/cityexplorer/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.cityexplorer; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/java/com/couchbase/cityexplorer/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.cityexplorer; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.util.Log; 8 | import android.view.Menu; 9 | import android.view.MenuItem; 10 | 11 | import com.couchbase.cityexplorer.model.Place; 12 | import com.couchbase.lite.CouchbaseLiteException; 13 | import com.couchbase.lite.Database; 14 | import com.couchbase.lite.Emitter; 15 | import com.couchbase.lite.LiveQuery; 16 | import com.couchbase.lite.Manager; 17 | import com.couchbase.lite.Mapper; 18 | import com.couchbase.lite.Query; 19 | import com.couchbase.lite.QueryEnumerator; 20 | import com.couchbase.lite.QueryRow; 21 | import com.couchbase.lite.View; 22 | import com.couchbase.lite.android.AndroidContext; 23 | import com.couchbase.lite.replicator.Replication; 24 | import com.couchbase.lite.support.LazyJsonObject; 25 | 26 | import java.io.IOException; 27 | import java.net.MalformedURLException; 28 | import java.net.URL; 29 | import java.util.ArrayList; 30 | import java.util.HashMap; 31 | import java.util.Iterator; 32 | import java.util.LinkedHashMap; 33 | import java.util.List; 34 | import java.util.Map; 35 | 36 | public class MainActivity extends AppCompatActivity { 37 | 38 | private static final String PLACES_VIEW = "getPlaces"; 39 | 40 | private Database database; 41 | private List currentRows; 42 | private PlacesAdapter adapter; 43 | private RecyclerView recyclerView; 44 | 45 | @Override 46 | protected void onCreate(Bundle savedInstanceState) { 47 | super.onCreate(savedInstanceState); 48 | setContentView(R.layout.activity_main); 49 | 50 | try { 51 | // replace with the IP to use 52 | URL url = new URL("http://192.168.1.218:4984/db"); 53 | 54 | Manager manager = new Manager(new AndroidContext(getApplicationContext()), Manager.DEFAULT_OPTIONS); 55 | 56 | database = manager.getExistingDatabase("cityexplorer"); 57 | if (database != null) { 58 | database.delete(); 59 | } 60 | database = manager.getDatabase("cityexplorer"); 61 | registerViews(); 62 | 63 | Replication pull = database.createPullReplication(url); 64 | pull.setContinuous(true); 65 | pull.start(); 66 | } catch (MalformedURLException e) { 67 | e.printStackTrace(); 68 | } catch (CouchbaseLiteException e) { 69 | e.printStackTrace(); 70 | } catch (IOException e) { 71 | e.printStackTrace(); 72 | } 73 | 74 | recyclerView = (RecyclerView) findViewById(R.id.list); 75 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 76 | 77 | adapter = new PlacesAdapter(this, new ArrayList(), database); 78 | recyclerView.setAdapter(adapter); 79 | 80 | final Query queryPlaces = database.getView(PLACES_VIEW).createQuery(); 81 | database.addChangeListener(new Database.ChangeListener() { 82 | @Override 83 | public void changed(Database.ChangeEvent event) { 84 | if (event.isExternal()) { 85 | QueryEnumerator rows = null; 86 | try { 87 | rows = queryPlaces.run(); 88 | } catch (CouchbaseLiteException e) { 89 | e.printStackTrace(); 90 | } 91 | List places = new ArrayList<>(); 92 | for (Iterator it = rows; it.hasNext(); ) { 93 | QueryRow row = it.next(); 94 | 95 | Log.d("", row.getValue().toString()); 96 | Map properties = database.getDocument(row.getDocumentId()).getProperties(); 97 | places.add(new Place((LazyJsonObject) row.getValue())); 98 | } 99 | 100 | adapter.dataSet = places; 101 | 102 | runOnUiThread(new Runnable() { 103 | @Override 104 | public void run() { 105 | recyclerView.getAdapter().notifyDataSetChanged(); 106 | } 107 | }); 108 | } 109 | } 110 | }); 111 | } 112 | 113 | public void deletePlace(android.view.View view) { 114 | Log.d("", "delete me"); 115 | adapter.dataSet.remove(2); 116 | try { 117 | database.getExistingDocument(adapter.dataSet.get(2).getId()).delete(); 118 | } catch (CouchbaseLiteException e) { 119 | e.printStackTrace(); 120 | } 121 | adapter.notifyItemRemoved(2); 122 | } 123 | 124 | private void registerViews() { 125 | View placesView = database.getView(PLACES_VIEW); 126 | placesView.setMap(new Mapper() { 127 | @Override 128 | public void map(Map document, Emitter emitter) { 129 | emitter.emit(document.get("_id"), document); 130 | } 131 | }, "1"); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/java/com/couchbase/cityexplorer/PlacesAdapter.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.cityexplorer; 2 | 3 | import android.content.Context; 4 | import android.graphics.drawable.Drawable; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.LinearLayout; 11 | import android.widget.TextView; 12 | 13 | import com.couchbase.cityexplorer.model.Place; 14 | import com.couchbase.lite.Attachment; 15 | import com.couchbase.lite.CouchbaseLiteException; 16 | import com.couchbase.lite.Database; 17 | import com.couchbase.lite.Document; 18 | 19 | import java.io.InputStream; 20 | import java.util.List; 21 | 22 | public class PlacesAdapter extends RecyclerView.Adapter { 23 | 24 | Context context; 25 | List dataSet; 26 | Database database; 27 | 28 | public PlacesAdapter(Context context, List dataSet, Database database) { 29 | this.context = context; 30 | this.dataSet = dataSet; 31 | this.database = database; 32 | } 33 | 34 | @Override 35 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 36 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_places, parent, false); 37 | return new ViewHolder(view); 38 | } 39 | 40 | @Override 41 | public void onBindViewHolder(ViewHolder holder, int position) { 42 | Place place = dataSet.get(position); 43 | 44 | holder.restaurantName.setText(place.getName()); 45 | holder.restaurantText.setText(place.getAddress()); 46 | 47 | Document document = database.getDocument(place.getId()); 48 | Attachment attachment = document.getCurrentRevision().getAttachment("photo"); 49 | if (attachment != null) { 50 | InputStream is = null; 51 | try { 52 | is = attachment.getContent(); 53 | } catch (CouchbaseLiteException e) { 54 | e.printStackTrace(); 55 | } 56 | Drawable drawable = Drawable.createFromStream(is, "photo"); 57 | holder.restaurantImage.setImageDrawable(drawable); 58 | } 59 | } 60 | 61 | @Override 62 | public int getItemCount() { 63 | return dataSet.size(); 64 | } 65 | 66 | public class ViewHolder extends RecyclerView.ViewHolder{ 67 | public TextView restaurantName; 68 | public TextView restaurantText; 69 | public ImageView restaurantImage; 70 | 71 | public ViewHolder(View itemView) { 72 | super(itemView); 73 | restaurantName = (TextView) itemView.findViewById(R.id.restaurantName); 74 | restaurantText = (TextView) itemView.findViewById(R.id.restaurantText); 75 | restaurantImage = (ImageView) itemView.findViewById(R.id.restaurantImage); 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/java/com/couchbase/cityexplorer/model/Place.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.cityexplorer.model; 2 | 3 | import com.couchbase.lite.support.LazyJsonObject; 4 | 5 | public class Place { 6 | private LazyJsonObject mLazy; 7 | 8 | public Place(LazyJsonObject lazyJsonObject) { 9 | mLazy = lazyJsonObject; 10 | } 11 | 12 | public String getName() { 13 | return (String) mLazy.get("name"); 14 | } 15 | 16 | public String getId() { 17 | return (String) mLazy.get("_id"); 18 | } 19 | 20 | public String getAddress() { 21 | return (String) mLazy.get("formatted_address"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 24 | 25 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/layout/row_places.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 20 | 21 | 26 | 27 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CityExplorer 3 | MainActivity 4 | 5 | Hello world! 6 | Settings 7 | 8 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.2.3' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/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 -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/Couchbase-by-Example/7adde5beb72bcc54fdea2fbd78d73024048939bb/05-android-recycler-view-animations/CityExplorer/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 23 14:35:58 BST 2015 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-2.4-all.zip 7 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /05-android-recycler-view-animations/CityExplorer/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /06-channels-users-roles/README.md: -------------------------------------------------------------------------------- 1 | # Couchbase by Example: Channel, Users, Roles 2 | 3 | In `04-ios-sync-progress-indicator`, you learnt how to use RxJS and the request module to import documents into a Sync Gateway database from the Google Places API. To keep it simple, you enabled the GUEST user with access to all channels. In this tutorial, you will configure the Sync Function to allow authenticated users to post reviews. 4 | 5 | The Sync Function validates document contents, and authorizes write access to documents by channel, user, and role. 6 | 7 | In total, there will be 3 types of roles in the application: 8 | 9 | - **level-1**: users with the **level-1** role can post reviews but they must be accepted by users with the **level-3** role (i.e. moderators) to be public. 10 | - **level-2**: users can post reviews without validation needed from moderators. This means they can post a comment without requiring an approval. 11 | - **level-3**: users can approve reviews or reject them. 12 | 13 | ## Download Sync Gateway 14 | 15 | Download Sync Gateway and unzip the file: 16 | 17 | > http://www.couchbase.com/nosql-databases/downloads#Couchbase\_Mobile 18 | 19 | You will find the Sync Gateway binary in the `bin` folder and examples of configuration files in the `examples` folder. Copy the `users-role.json` file to the root of your project: 20 | 21 | ```bash 22 | cp ~/Downloads/couchbase-sync-gateway/examples/users-roles.json /path/to/proj/sync-gateway-config.json 23 | ``` 24 | 25 | In the next section, you will update the configuration file to create users and roles. 26 | 27 | ## Channels, Users and Roles 28 | 29 | In `sync-gateway-config.json`, update the db object to read: 30 | 31 | ```javascript 32 | { 33 | "log": ["*"], 34 | "databases": { 35 | "db": { 36 | "server": "walrus:", 37 | "users": { 38 | "jens": { 39 | "admin_roles": ["level-1"], 40 | "password": "letmein" 41 | }, 42 | "andy": { 43 | "admin_roles": ["level-2"], 44 | "password": "letmein" 45 | }, 46 | "william": { 47 | "admin_roles": [], 48 | "password": "letmein" 49 | }, 50 | "traun": { 51 | "admin_roles": ["level-3"], 52 | "password": "letmein" 53 | } 54 | }, 55 | "roles": { 56 | "level-1": {}, 57 | "level-2": {}, 58 | "level-3": {} 59 | }, 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | A couple of things are happening above: 66 | 67 | 1. You create the user `jens` with the `level-1` role. 68 | 2. You create the user `andy` with the `level-2` role. 69 | 3. You create the user `william` without any role. 70 | 4. You create the user `traun` with the `level-3` role. 71 | 5. You define the 3 roles. Just like users, roles must be explicitly created on the Admin REST API or in the config file. 72 | 73 | **Note on creating roles** 74 | 75 | The easiest way to create roles is in the configuration file as you did above. 76 | 77 | Another way to create roles is through the admin REST API. Provided that you expose an endpoint to create those roles from the application, you can create roles dynamically by sending a request to your app server (blue arrows) which will create the role and send back a 201 Created if it was successful (green arrows). 78 | 79 | ![](http://cl.ly/image/3D0606230F1C/Dynamic%20Roles.png) 80 | 81 | In the next section, you will add the Sync Function to handle write and read operations for the three different types of documents (`restaurant`, `review`, `profile`). 82 | 83 | ## Sync Function 84 | 85 | Roles and users can both be granted access to channels. Users can be granted roles, and inherit any channel access for those roles. 86 | 87 | Channel access determines a user’s read security. Write security can also be based on channels (using requireAccess), but can also be based on user/role (requireUser and requireRole), or document content (using throw). 88 | 89 | Read and write access to documents are independent. In fact write access is entirely governed by your sync function: unless the sync function rejects the revision, a client can modify any document. All the require* functions act as validators but also write access APIs. 90 | 91 | It's very common to see sync function creating lots and lots of channels. This is absolutely fine. However, it can get cumbersome to assign each user in turn to a channel. Instead you can use a role! 92 | 93 | Let this sink in one more time, users can be granted roles and inherit any channel access for those roles. 94 | 95 | This means you can grant a user access to multiple channels by simply assigning a role. This is very powerful and can greatly simplify the data model in your application. 96 | 97 | With roles, you don't need to assign every single user to a channel. You simply grant the role access to the channel and assign the users to the role. 98 | 99 | With that in mind, replace the sync function in `sync-gateway-config.json`: 100 | 101 | ```javascript 102 | function(doc, oldDoc) { 103 | if (doc.type == "restaurant"){ 104 | channel(doc.restaurant_id); 105 | } else if (doc.type == "review") { 106 | switch(doc.role) { 107 | case "level-1": // Step 1 108 | requireRole(doc.role); 109 | var channelname = doc.owner + "-in-review"; 110 | channel(channelname); 111 | access(doc.owner, channelname); 112 | access("role:level-3", channelname); 113 | break; 114 | case "level-2": // Step 2 115 | requireRole(doc.role); 116 | channel(doc.restaurant_id); 117 | break; 118 | case "level-3": // Step 3 119 | requireRole(doc.role); 120 | channel(doc.restaurant_id); 121 | break; 122 | } 123 | } else if (doc.type == "profile") { 124 | requireRole("level-3"); 125 | role(doc.name, "role:" + doc.role); 126 | } 127 | } 128 | ``` 129 | 130 | Here's what's happening: 131 | 132 | 1. Users with the **level-1** role have write access because you call the `channel` function. Then grant that user and the **level-3** access to this channel. This is where the power of roles really shines. By granting a role access, you are granting all the users with that role access to the channel. The call to `requireRole` will grant the write permission. 133 | 2. Documents of type `review` created by a **level-2** role: the document should go in the same channel as the restaurant it belongs to. The call to `requireRole` will grant the write permission. 134 | 3. Documents of type `review` created by a **level-3** role: the document should go in the channel for that restaurant. **level-3** users also have read access to all the `{user_name}-in-review` channels. They can approve/reject the pending reviews of other users. 135 | 136 | Start Sync Gateway with the updated configuration file: 137 | 138 | ```bash 139 | $ ~/Downloads/couchbase-sync-gateway/bin/sync_gateway /path/to/proj/sync-gateway-config.json 140 | ``` 141 | 142 | In this example, you are utilising the 3 main features of roles: 143 | 144 | - Granting a role access to a channel and indirectly to all the users with that role. 145 | - Granting write permission using a requireRole. 146 | - Assigning a role to a user. 147 | 148 | Now you can test the Sync Function behaves as expected with the following HTTP requests. 149 | 150 | **Scenario 1** 151 | 152 | Documents of type `review` created by a **level-1** user: the document should go in the `{user_name}-in-review` channel and the users with the **level-3** role should have access to this channel too. 153 | 154 | Login as the user `jens`: 155 | 156 | ```bash 157 | curl -vX POST -H 'Content-Type: application/json' \ 158 | :4984/db/_session \ 159 | -d '{"name": "jens", "password": "letmein"}' 160 | 161 | > POST /db/_session HTTP/1.1 162 | > User-Agent: curl/7.37.1 163 | > Host: :4984 164 | > Accept: */* 165 | > Content-Type: application/json 166 | > Content-Length: 39 167 | > 168 | * upload completely sent off: 39 out of 39 bytes 169 | < HTTP/1.1 200 OK 170 | < Content-Length: 103 171 | < Content-Type: application/json 172 | * Server Couchbase Sync Gateway/1.1.0 is not blacklisted 173 | < Server: Couchbase Sync Gateway/1.1.0 174 | < Set-Cookie: SyncGatewaySession=6c52b8cd2c706d55e97d9606058c0abd90a5d200; Path=/db/; Expires=Tue, 07 Jul 2015 08:23:03 UTC 175 | < Date: Mon, 06 Jul 2015 08:23:03 GMT 176 | < 177 | * Connection #0 to host left intact 178 | {"authentication_handlers":["default","cookie"],"ok":true,"userCtx":{"channels":{"!":1},"name":"jens"}}⏎ 179 | ``` 180 | 181 | Save a new document of type `review` (substitute the token with the one returned in the `Set-Cookie` header above): 182 | 183 | ```bash 184 | curl -vX POST -H 'Content-Type: application/json' \ 185 | --cookie 'SyncGatewaySession=d007ceb561f0111512c128040c32c02ea9d90234' \ 186 | :4984/db/ \ 187 | -d '{"type": "review", "role": "level-1", "owner": "jens"}' 188 | ``` 189 | 190 | - Check that user `jens` has access to the channel `jens-in-review` and the comment document is in there. 191 | 192 | ![](http://cl.ly/image/190111230227/Screen%20Shot%202015-07-06%20at%2010.48.44.png) 193 | 194 | - Check that user `traun` has access to channel `jens-in-review`. 195 | 196 | ![](http://cl.ly/image/2j2f0z2z1K0M/Screen%20Shot%202015-07-06%20at%2010.50.13.png) 197 | 198 | You can also view the channels this document belongs to and roles/users that have access to it in the `Documents` tab: 199 | 200 | ![](http://cl.ly/image/1p3J410N0L2C/Screen%20Shot%202015-07-06%20at%2010.53.19.png) 201 | 202 | **Scenario 2** 203 | 204 | Granting write access using a role. 205 | 206 | Login as `andy` and replace the token with the one you got back from the login request. 207 | 208 | ```bash 209 | curl -vX POST -H 'Content-Type: application/json' \ 210 | --cookie 'SyncGatewaySession=6e7ce145ae53c83de436b47ae37d8d94beebebea' \ 211 | :4984/db/ \ 212 | -d '{"type": "review", "role": "level-2", "owner": "andy", "restaurant_id": "123"}' 213 | ``` 214 | 215 | - Check that the comment was added to the restaurant channel (named `123` in this example). 216 | 217 | ![](http://cl.ly/image/1g283S032M0w/Screen%20Shot%202015-07-06%20at%2010.53.01.png) 218 | 219 | **Scenario 3** 220 | 221 | Assigning a role to a user. 222 | 223 | Login as `traun` and replace the token with the one you got back from the login request. 224 | 225 | ```bash 226 | curl -vX POST -H 'Content-Type: application/json' \ 227 | --cookie 'SyncGatewaySession=3a5c5a67ff67643f8ade175363c65354584429e9' \ 228 | :4984/db/ \ 229 | -d '{"type": "profile", "name": "william", "role": "level-3"}' 230 | ``` 231 | 232 | - Check that `william` has role `level-3`. 233 | - Check that `william` has access to the `jens-in-review` channel. 234 | 235 | ![](http://cl.ly/image/092F173R350B/Screen%20Shot%202015-07-06%20at%2010.55.37.png) 236 | 237 | ## Conclusion 238 | 239 | In this tutorial, you learnt how to use channels and requireRole to dynamically validate and perform write operations. You also assigned multiple channels at once to multiple users using the role API. -------------------------------------------------------------------------------- /06-channels-users-roles/sync-gateway-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": ["*"], 3 | "databases": { 4 | "db": { 5 | "server": "walrus:", 6 | "users": { 7 | "jens": { 8 | "admin_roles": ["level-1"], 9 | "password": "letmein" 10 | }, 11 | "andy": { 12 | "admin_roles": ["level-2"], 13 | "password": "letmein" 14 | }, 15 | "william": { 16 | "admin_roles": [], 17 | "password": "letmein" 18 | }, 19 | "traun": { 20 | "admin_roles": ["level-3"], 21 | "password": "letmein" 22 | } 23 | }, 24 | "roles": { 25 | "level-1": {}, 26 | "level-2": {}, 27 | "level-3": {} 28 | }, 29 | "sync": ` 30 | function(doc, oldDoc) { 31 | if (doc.type == "restaurant"){ 32 | channel(doc.restaurant_id); 33 | } else if (doc.type == "review") { 34 | switch(doc.role) { 35 | case "level-1": // Step 1 36 | requireRole(doc.role); 37 | var channelname = doc.owner + "-in-review"; 38 | channel(channelname); 39 | 40 | access(doc.owner, channelname); 41 | access("role:level-3", channelname); 42 | 43 | break; 44 | case "level-2": // Step 2 45 | requireRole(doc.role); 46 | channel(doc.restaurant_id); 47 | 48 | break; 49 | case "level-3": // Step 3 50 | requireRole(doc.role); 51 | channel(doc.restaurant_id); 52 | 53 | break; 54 | } 55 | } else if (doc.type == "profile") { 56 | requireRole("level-3"); 57 | role(doc.name, "role:" + doc.role); 58 | } 59 | } 60 | ` 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /07-deploy-digital-ocean/Dockerfile: -------------------------------------------------------------------------------- 1 | # Set the base image to Ubuntu 2 | FROM ubuntu 3 | 4 | # Install Node.js and other dependencies 5 | RUN apt-get update && \ 6 | apt-get -y install curl && \ 7 | curl -sL https://deb.nodesource.com/setup | sudo bash - && \ 8 | apt-get -y install python build-essential nodejs 9 | 10 | # Install nodemon 11 | RUN npm install -g babel 12 | 13 | # Provides cached layer for node_modules 14 | ADD package.json /tmp/package.json 15 | RUN cd /tmp && npm install 16 | RUN mkdir -p /src && cp -a /tmp/node_modules /src/ 17 | 18 | # Define working directory 19 | WORKDIR /src 20 | ADD . /src -------------------------------------------------------------------------------- /07-deploy-digital-ocean/README.md: -------------------------------------------------------------------------------- 1 | ## Couchbase by Example: Deploy Digital Ocean 2 | 3 | So far, you have been running Sync Gateway and perhaps Couchbase Server locally to follow the tutorials. Now it's time to learn how to easily deploy both applications and perhaps an App Server sitting alongside Sync Gateway to a PaaS. 4 | 5 | You will use the Docker images for Sync Gateway and [Couchbase Server](https://registry.hub.docker.com/u/couchbase/server/) available in the Docker registry. 6 | 7 | Then, you will create a `Dockerfile` for a NodeJS app that imports data from the Google Places API to Sync Gateway. The NodeJS app is taken from the `04-ios-sync-progress-indicator` tutorial. 8 | 9 | ## Why Docker? 10 | 11 | If you've ever done any application development and deployment then you know how difficult it can be to ensure that your development and production servers are the same or at least similar enough that it is not causing any major issue. 12 | 13 | With docker, you can build a container that houses everything you need to configure your application dependencies and services. 14 | 15 | This container can then be shared and run on any server or computer without having to do a whole bunch of setup and configuration. 16 | 17 | Create a new Digital Ocean Droplet 18 | 19 | > https://cloud.digitalocean.com/droplets/new 20 | 21 | Pick 2GB for the RAM: 22 | 23 | ![](http://cl.ly/image/2D2w0H0a0a2W/Screen%20Shot%202015-07-08%20at%2009.27.21.png) 24 | 25 | Choose Ubuntu for the distribution and on the Applications tab, select Docker. Create the Droplet and connect to it via SSH. 26 | 27 | ## Installing Sync Gateway and Couchbase Server 28 | 29 | In the command line, install Couchbase Server with the following: 30 | 31 | ``` 32 | docker run -d --name db -p 8091-8094:8091-8094 -p 11210:11210 couchbase 33 | ``` 34 | 35 | Open the getting started wizard in Chrome on port `http://:8091`: 36 | 37 | ![](http://cl.ly/image/2o07072W1l3T/Screen%20Shot%202015-07-08%20at%2009.47.34.png) 38 | 39 | After completing the wizard, navigate to the `Data Buckets` tab to see the created bucket (you will configure Sync Gateway to connect to the `default` bucket): 40 | 41 | ![](http://cl.ly/image/0s1f3m2O1v1r/Screen%20Shot%202015-07-08%20at%2009.48.47.png) 42 | 43 | Copy the necessary files from the `04-ios-sync-progress` tutorial to this project: 44 | 45 | ``` 46 | git clone git@github.com:couchbaselabs/Couchbase-by-Example.git 47 | cd couchbase-by-example/04-ios-sync-progress-indicator 48 | cp requestRx.js sync-gateway-config.json sync.js package.json ./../07-deploy-digital-ocean/ 49 | ``` 50 | 51 | Create a new copy of the config file to set it up with Couchbase Server: 52 | 53 | ``` 54 | cd 07-deploy-digital-ocean/ 55 | cp sync-gateway-config.json production-sync-gateway-config.json 56 | ``` 57 | 58 | Update `production-sync-gateway-config.json` with the server IP and bucket name: 59 | 60 | ```javascript 61 | { 62 | "log": ["*"], 63 | "databases": { 64 | "db": { 65 | "server": "http://46.101.14.135:8091/", 66 | "bucket": "default", 67 | "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } } 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | Push the files to a github repository. 74 | 75 | Specify the url to the production config file in the docker command to run the Sync Gateway container: 76 | 77 | ```bash 78 | $ docker run -d -p 4984:4984 -p 4985:4985 couchbase/sync-gateway http://git.io/vq25r 79 | ``` 80 | 81 | **NOTE**: You ran `docker run` but this time specified the `-d` flag. It tells Docker to run the container and put it in the background, to daemonize it. 82 | 83 | Run the `docker ps` command to check that both containers are running: 84 | 85 | ``` 86 | root@MyApp:~# docker ps 87 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 88 | ca7d4358941a couchbase/sync-gateway "/usr/local/bin/sync 2 minutes ago Up 2 minutes 0.0.0.0:4984-4985->4984-4985/tcp focused_bell 89 | c9411d002831 couchbase/server "couchbase-start cou 52 minutes ago Up 52 minutes 8092/tcp, 11207/tcp, 11210-11211/tcp, 0.0.0.0:8091->8091/tcp, 18091-18092/tcp grave_feynman 90 | ``` 91 | 92 | Use the `docker logs` command specifying the container id to print the STDOUT to your console. 93 | 94 | **TIP**: Use the `-f` flag to follow the logs. 95 | 96 | In the next section, you will write a simple Dockerfile to deploy the NodeJS application to the same Droplet. 97 | 98 | ## Deploying an App Server 99 | 100 | Open a new file named `Dockerfile` and paste the following: 101 | 102 | ``` 103 | # Set the base image to Ubuntu 104 | FROM ubuntu 105 | 106 | # Install Node.js and other dependencies 107 | RUN apt-get update && \ 108 | apt-get -y install curl && \ 109 | curl -sL https://deb.nodesource.com/setup | sudo bash - && \ 110 | apt-get -y install python build-essential nodejs 111 | 112 | # Install nodemon 113 | RUN npm install -g babel 114 | 115 | # Provides cached layer for node_modules 116 | ADD package.json /tmp/package.json 117 | RUN cd /tmp && npm install 118 | RUN mkdir -p /src && cp -a /tmp/node_modules /src/ 119 | 120 | # Define working directory 121 | WORKDIR /src 122 | ADD . /src 123 | ``` 124 | 125 | - Ubuntu base image pulled from Docker Hub 126 | - Install Node.js and dependencies using apt-get 127 | - Install babel to run `sync.js` because it's written in ES6. 128 | - Run npm install in a temporary directory and copy to src (for caching node_modules) 129 | - Copy the application source from the host directory to src within the container 130 | 131 | Build a Docker image using the Dockerfile: 132 | 133 | ``` 134 | docker build -t myapp . 135 | ``` 136 | 137 | Create a container for the custom image: 138 | ``` 139 | docker run -it myapp /bin/bash 140 | ``` 141 | 142 | **NOTE**: The `-i` flag is used to keep STDIN open and `-t` to allocate a pseudo-TTY. 143 | 144 | Notice the command line prompt in the container. From there you can run the import script: 145 | 146 | ``` 147 | babel-node sync.js 148 | ``` 149 | 150 | Use `Ctrl + D` to exit the container. 151 | 152 | You can push the Dockerfile to the GitHub repo and pull the changes in the droplet. Then run the same commands to build and run the image. 153 | 154 | ## Conclusion 155 | 156 | Yay! Now you know how to use the docker images to deploy Sync Gateway and Couchbase Server and creating your own images for other applications running alongside them. 157 | 158 | ## Extra: Accessing the Admin Dashboard 159 | 160 | The Admin Dashboard is available on `http://localhost:4985/_admin/` and only accessible on the internal network where Sync Gateway is running. 161 | 162 | However, you can use SSH tunnelling to create a connection with the droplet. This currently doesn't work with Sync Gateway instances running in Docker containers. 163 | 164 | If you already have a Docker instance running Sync Gateway, you can stop it with: 165 | 166 | ``` 167 | docker stop 168 | ``` 169 | 170 | Install Sync Gateway with wget: 171 | 172 | ``` 173 | wget http://packages.couchbase.com/releases/couchbase-sync-gateway/1.0.0/couchbase-sync-gateway-community_1.0.0_x86_64.deb 174 | sudo dpkg -i couchbase-sync-gateway-community_1.0.0_x86_64.deb 175 | ``` 176 | 177 | By default it will be installed to **/opt/couchbase-sync-gateway/bin/sync_gateway**. 178 | 179 | Start it with the command: 180 | 181 | ``` 182 | $ /opt/couchbase-sync-gateway/bin/sync_gateway 183 | ``` 184 | 185 | ### Creating an SSH tunnel 186 | 187 | Open a terminal on your host machine and enter the following command where is the public ip address of the droplet: 188 | 189 | ``` 190 | ssh -ND 8080 root@ 191 | ``` 192 | 193 | **Explanation** 194 | 195 | - N: hides the output from the SSH connection. It is optional. If you wish to use the SSH connection to run commands on the server as you normally would, remove the N switch. 196 | - D: 8080 creates a dynamic port, in this case 8080, on your local computer. This is how your browser, or other software, will connect to the tunnel. 197 | 198 | ### Using as a browser proxy 199 | 200 | 1. Open `System Preferences > Network > Advanced > Proxies` 201 | 2. Check the checkbox next to SOCKS Proxy 202 | 3. Under SOCKS Proxy Server, enter `localhost` and `8080` as the port. Leave all the other fields blank. 203 | ![](http://cl.ly/image/2v1l1W002T3T/Screen%20Shot%202015-07-08%20at%2015.02.56.png) 204 | 4. Click OK and Apply. 205 | 206 | Open `http://localhost:4985/_admin` in Chrome and you should see the Admin Dashboard. 207 | 208 | ![](http://cl.ly/image/0E0F2b200P0Y/Screen%20Shot%202015-07-08%20at%2015.04.30.png) 209 | 210 | **Reference** 211 | 212 | > https://whatbox.ca/wiki/SSH_Tunneling 213 | -------------------------------------------------------------------------------- /08-signup-and-login/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/SmartHome.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.couchbase.smarthome" 9 | minSdkVersion 7 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | 25 | compile 'com.android.support:design:22.2.1' 26 | compile 'com.squareup.okhttp:okhttp:2.3.0' 27 | } 28 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/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 /Users/jamesnocentini/Developer/adt-bundle/sdk/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 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/app/src/androidTest/java/com/couchbase/smarthome/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.smarthome; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/app/src/main/java/com/couchbase/smarthome/Login.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.smarthome; 2 | 3 | import android.app.Activity; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.view.Menu; 7 | import android.view.MenuItem; 8 | import android.view.View; 9 | import android.widget.EditText; 10 | import android.widget.Toast; 11 | 12 | import com.squareup.okhttp.Call; 13 | import com.squareup.okhttp.Callback; 14 | import com.squareup.okhttp.MediaType; 15 | import com.squareup.okhttp.OkHttpClient; 16 | import com.squareup.okhttp.Request; 17 | import com.squareup.okhttp.RequestBody; 18 | import com.squareup.okhttp.Response; 19 | 20 | import java.io.IOException; 21 | 22 | public class Login extends Activity { 23 | 24 | NetworkHelper networkHelper = new NetworkHelper(); 25 | 26 | EditText nameInput; 27 | EditText passwordInput; 28 | 29 | @Override 30 | protected void onCreate(Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | setContentView(R.layout.activity_login); 33 | 34 | nameInput = (EditText) findViewById(R.id.nameInput); 35 | passwordInput = (EditText) findViewById(R.id.passwordInput); 36 | } 37 | 38 | @Override 39 | public boolean onCreateOptionsMenu(Menu menu) { 40 | // Inflate the menu; this adds items to the action bar if it is present. 41 | getMenuInflater().inflate(R.menu.menu_login, menu); 42 | return true; 43 | } 44 | 45 | @Override 46 | public boolean onOptionsItemSelected(MenuItem item) { 47 | // Handle action bar item clicks here. The action bar will 48 | // automatically handle clicks on the Home/Up button, so long 49 | // as you specify a parent activity in AndroidManifest.xml. 50 | int id = item.getItemId(); 51 | 52 | //noinspection SimplifiableIfStatement 53 | if (id == R.id.action_settings) { 54 | return true; 55 | } 56 | 57 | return super.onOptionsItemSelected(item); 58 | } 59 | 60 | public void login(View view) { 61 | String json = "{\"name\": \"" + nameInput.getText() + "\", \"password\":\"" + passwordInput.getText() + "\"}"; 62 | networkHelper.post("http://10.0.3.2:8000/smarthome/_session", json, new Callback() { 63 | @Override 64 | public void onFailure(Request request, IOException e) { 65 | 66 | } 67 | 68 | @Override 69 | public void onResponse(Response response) throws IOException { 70 | String responseStr = response.body().string(); 71 | final String messageText = "Status code : " + response.code() + 72 | "\n" + 73 | "Response body : " + responseStr; 74 | runOnUiThread(new Runnable() { 75 | @Override 76 | public void run() { 77 | Toast.makeText(getApplicationContext(), messageText, Toast.LENGTH_LONG).show(); 78 | } 79 | }); 80 | } 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/app/src/main/java/com/couchbase/smarthome/SignUp.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.smarthome; 2 | 3 | import android.app.Activity; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.view.Menu; 7 | import android.view.MenuItem; 8 | import android.view.View; 9 | import android.widget.EditText; 10 | import android.widget.Toast; 11 | 12 | import com.squareup.okhttp.Call; 13 | import com.squareup.okhttp.Callback; 14 | import com.squareup.okhttp.MediaType; 15 | import com.squareup.okhttp.OkHttpClient; 16 | import com.squareup.okhttp.Request; 17 | import com.squareup.okhttp.RequestBody; 18 | import com.squareup.okhttp.Response; 19 | 20 | import java.io.IOException; 21 | 22 | public class SignUp extends Activity { 23 | 24 | NetworkHelper networkHelper = new NetworkHelper(); 25 | 26 | EditText nameInput; 27 | EditText passwordInput; 28 | EditText confirmPasswordInput; 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(R.layout.activity_sign_up); 34 | 35 | nameInput = (EditText) findViewById(R.id.nameInput); 36 | passwordInput = (EditText) findViewById(R.id.passwordInput); 37 | confirmPasswordInput = (EditText) findViewById(R.id.confirmPasswordInput); 38 | } 39 | 40 | @Override 41 | public boolean onCreateOptionsMenu(Menu menu) { 42 | // Inflate the menu; this adds items to the action bar if it is present. 43 | getMenuInflater().inflate(R.menu.menu_sign_up, menu); 44 | return true; 45 | } 46 | 47 | @Override 48 | public boolean onOptionsItemSelected(MenuItem item) { 49 | // Handle action bar item clicks here. The action bar will 50 | // automatically handle clicks on the Home/Up button, so long 51 | // as you specify a parent activity in AndroidManifest.xml. 52 | int id = item.getItemId(); 53 | 54 | //noinspection SimplifiableIfStatement 55 | if (id == R.id.action_settings) { 56 | return true; 57 | } 58 | 59 | return super.onOptionsItemSelected(item); 60 | } 61 | 62 | public void signup(View view) { 63 | if (!passwordInput.getText().toString().equals(confirmPasswordInput.getText().toString())) { 64 | Toast.makeText(getApplicationContext(), "The passwords do not match", Toast.LENGTH_LONG).show(); 65 | } else { 66 | String json = "{\"name\": \"" + nameInput.getText() + "\", \"password\":\"" + passwordInput.getText() + "\"}"; 67 | networkHelper.post("http://10.0.3.2:8000/signup", json, new Callback() { 68 | @Override 69 | public void onFailure(Request request, IOException e) { 70 | 71 | } 72 | 73 | @Override 74 | public void onResponse(Response response) throws IOException { 75 | String responseStr = response.body().string(); 76 | final String messageText = "Status code : " + response.code() + 77 | "\n" + 78 | "Response body : " + responseStr; 79 | runOnUiThread(new Runnable() { 80 | @Override 81 | public void run() { 82 | Toast.makeText(getApplicationContext(), messageText, Toast.LENGTH_LONG).show(); 83 | } 84 | }); 85 | } 86 | }); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/app/src/main/java/com/couchbase/smarthome/WelcomeActivity.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.smarthome; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.Menu; 7 | import android.view.MenuItem; 8 | import android.view.View; 9 | 10 | public class WelcomeActivity extends Activity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | setContentView(R.layout.activity_welcome); 16 | } 17 | 18 | @Override 19 | public boolean onCreateOptionsMenu(Menu menu) { 20 | // Inflate the menu; this adds items to the action bar if it is present. 21 | getMenuInflater().inflate(R.menu.menu_welcome, menu); 22 | return true; 23 | } 24 | 25 | @Override 26 | public boolean onOptionsItemSelected(MenuItem item) { 27 | // Handle action bar item clicks here. The action bar will 28 | // automatically handle clicks on the Home/Up button, so long 29 | // as you specify a parent activity in AndroidManifest.xml. 30 | int id = item.getItemId(); 31 | 32 | //noinspection SimplifiableIfStatement 33 | if (id == R.id.action_settings) { 34 | return true; 35 | } 36 | 37 | return super.onOptionsItemSelected(item); 38 | } 39 | 40 | public void openLoginActivity(View view) { 41 | Intent intent = new Intent(this, Login.class); 42 | startActivity(intent); 43 | } 44 | 45 | public void openSignUpActivity(View view) { 46 | Intent intent = new Intent(this, SignUp.class); 47 | startActivity(intent); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | x 12 | x 13 | x 14 | x 15 | x 16 | 17 | x 18 | x 19 | x 20 | x 21 | x 22 | 23 | x 24 | x 25 | x 26 | 27 | x 28 | x 29 | x 30 | x 31 | x 32 | x 33 | 34 | x 35 | x 36 | x 37 | 38 | x 39 | x 40 | x 41 | x 42 | x 43 | x 44 | 45 | x 46 | x 47 | x 48 | x 49 | x 50 | x 51 | x 52 | 53 | x 54 | 55 | 56 | -------------------------------------------------------------------------------- /08-signup-and-login/SmartHome/app/src/main/res/layout/activity_sign_up.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 22 | 23 | 26 | 27 | 32 | 33 | 34 | 37 | 38 | 43 | 44 | 45 | 48 | 49 | 54 | 55 | 56 |