├── .gitignore ├── .watchmanconfig ├── App.js ├── README.md ├── app.json ├── assets ├── icon.png └── splash.png ├── babel.config.js ├── package-lock.json ├── package.json ├── server ├── AFIDetailsVM.cs ├── AFITop100.json ├── AFITop100ListVM.cs ├── AuthServer.cs ├── LiveGaugeVM.cs ├── MovieService.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs └── server.csproj └── src ├── AppNavigation.js ├── Authentication.js ├── ScreenTracker.js └── screens ├── AFIDetailsScreen.js ├── AFITop100Screen.js ├── LiveGaugeScreen.js └── LoginScreen.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | /server/*.user 9 | /server/.vs 10 | /server/obj 11 | /server/bin 12 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, Platform, StatusBar, StyleSheet, Text, View } from 'react-native'; 3 | import { AppLoading, Font } from 'expo'; 4 | import { Ionicons } from '@expo/vector-icons'; 5 | import dotnetify from 'dotnetify/react-native'; 6 | import signalRnetfx from 'dotnetify/dist/signalR-netfx'; 7 | import AppNavigation from './src/AppNavigation'; 8 | import ScreenTracker from './src/ScreenTracker'; 9 | import Authentication from './src/Authentication'; 10 | 11 | const androidEmulatorServerUrl = 'http://169.254.80.80:5000'; 12 | const liveServerUrl = 'http://dotnetify.net'; 13 | const serverUrl = Platform.OS === 'android' ? androidEmulatorServerUrl : liveServerUrl; 14 | 15 | dotnetify.debug = true; 16 | 17 | // Live server is still running an older signalR version, which requires a different SignalR client library. 18 | if (serverUrl == liveServerUrl) { 19 | dotnetify.hubLib = signalRnetfx; 20 | dotnetify.hubServerUrl = serverUrl + '/signalr'; 21 | dotnetify.hubOptions.pingInterval = 60000; 22 | } 23 | else dotnetify.hubServerUrl = serverUrl; 24 | 25 | Authentication.url = serverUrl + '/token'; 26 | 27 | export default class App extends React.Component { 28 | state = { appLoaded: false, connectionStatus: null }; 29 | 30 | constructor(props) { 31 | super(props); 32 | 33 | dotnetify.connectionStateHandler = (state, ex) => { 34 | this.setState({ connectionStatus: state == 'connected' ? null : state }); 35 | if (state == 'error') Alert.alert('Connection Error', ex.message, [ { text: 'OK' } ], { cancelable: false }); 36 | }; 37 | } 38 | 39 | componentWillMount() { 40 | Font.loadAsync(Ionicons.font); 41 | this.setState({ appLoaded: true }); 42 | ScreenTracker.setScreen('LiveGauge'); 43 | } 44 | 45 | handleNavigationStateChange(prevState, newState) { 46 | ScreenTracker.setScreen(newState); 47 | } 48 | 49 | render() { 50 | if (!this.state.appLoaded) return ; 51 | 52 | return ( 53 | 54 | {Platform.OS === 'ios' && } 55 | {Platform.OS === 'android' && } 56 | {this.state.connectionStatus ? {this.state.connectionStatus} : null} 57 | 58 | 59 | ); 60 | } 61 | } 62 | 63 | const styles = StyleSheet.create({ 64 | container: { 65 | flex: 1, 66 | backgroundColor: '#fff' 67 | }, 68 | statusBarUnderlay: { 69 | height: 24, 70 | backgroundColor: 'rgba(0,0,0,0.2)' 71 | }, 72 | error: { 73 | backgroundColor: 'red', 74 | color: 'white', 75 | textAlign: 'center' 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #  ![alt tag](http://dotnetify.net/content/images/greendot.png) dotNetify-React-Native Demo 2 | DotNetify makes it super easy to connect your React Native mobile app to a cross-platform .NET back-end and get real-time two-way communication with WebSockets for free! 3 | 4 | ### Demo Features 5 | 6 | - Reactive push model allows for a very light-weight client. No need to set up REST or GraphQL calls; state change can cause new data to be pushed directly into the component's state. 7 | - Token-based authentication over websocket communications with expiration handling. 8 | - Infinite list scrolling. 9 | - Live data display for IoT use cases. 10 | - Connection error handling. 11 | - Using react-navigation with custom logic to track active screen. 12 | 13 | ### How To Run 14 | Install [Node.js](https://nodejs.org) and [.NET Core SDK](https://www.microsoft.com/net/core#windowscmd). 15 | 16 | Install [create-react-native-app](https://facebook.github.io/react-native/blog/2017/03/13/introducing-create-react-native-app.html): 17 | ``` 18 | npm i -g create-react-native-app 19 | ``` 20 | 21 | Follow the [instructions on Expo website](https://docs.expo.io/versions/latest/introduction/installation.html) to install Genymotion Android emulator and/or download the Expo app for iOS or Android. 22 | 23 | Run the client on a terminal: 24 | ``` 25 | npm install 26 | expo start 27 | ``` 28 | Open another terminal and run the server (ASP.NET Core 2.1): 29 | ``` 30 | cd server 31 | dotnet build 32 | dotnet run 33 | ``` 34 | 35 | ### Android Demo 36 | 37 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "dotnetify-react-native-demo", 4 | "description": "This project is really great.", 5 | "slug": "dotnetify-react-native-demo", 6 | "privacy": "public", 7 | "sdkVersion": "31.0.0", 8 | "platforms": ["ios", "android"], 9 | "version": "1.0.0", 10 | "orientation": "portrait", 11 | "icon": "./assets/icon.png", 12 | "splash": { 13 | "image": "./assets/splash.png", 14 | "resizeMode": "contain", 15 | "backgroundColor": "#ffffff" 16 | }, 17 | "updates": { 18 | "fallbackToCacheTimeout": 0 19 | }, 20 | "assetBundlePatterns": [ 21 | "**/*" 22 | ], 23 | "ios": { 24 | "supportsTablet": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-react-native-demo/61e75f0062b82d177a46f2b603a82f8c1b714b85/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-react-native-demo/61e75f0062b82d177a46f2b603a82f8c1b714b85/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "empty-project-template", 3 | "main": "node_modules/expo/AppEntry.js", 4 | "private": true, 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "eject": "expo eject" 10 | }, 11 | "dependencies": { 12 | "dotnetify": "^3.3.1", 13 | "expo": "~31.0.5", 14 | "react": "16.5.0", 15 | "react-native": "https://github.com/expo/react-native/archive/sdk-31.0.0.tar.gz", 16 | "react-native-elements": "~0.19.1", 17 | "react-native-simple-gauge": "~0.1.11", 18 | "react-navigation": "~1.5.8" 19 | }, 20 | "devDependencies": { 21 | "babel-preset-expo": "^5.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/AFIDetailsVM.cs: -------------------------------------------------------------------------------- 1 | using DotNetify; 2 | using DotNetify.Security; 3 | 4 | namespace server 5 | { 6 | [Authorize] 7 | public class AFIDetailsVM : BaseVM 8 | { 9 | private readonly MovieService _movieService; 10 | 11 | public int Rank 12 | { 13 | get { return Get(); } 14 | set 15 | { 16 | Set(value); 17 | Changed(nameof(MovieDetails)); 18 | } 19 | } 20 | 21 | public AFIDetailsVM(MovieService movieService) 22 | { 23 | _movieService = movieService; 24 | } 25 | 26 | public MovieRecord MovieDetails => _movieService.GetMovieByAFIRank(Rank); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/AFITop100.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Rank": 1, 4 | "Movie": "Citizen Kane", 5 | "Year": 1941, 6 | "Cast": "Orson Welles, Joseph Cotten, Dorothy Comingore", 7 | "Director": "Orson Welles" 8 | }, 9 | { 10 | "Rank": 2, 11 | "Movie": "The Godfather", 12 | "Year": 1972, 13 | "Cast": "Marlon Brando, Al Pacino, James Caan", 14 | "Director": "Francis Ford Coppola" 15 | }, 16 | { 17 | "Rank": 3, 18 | "Movie": "Casablanca", 19 | "Year": 1942, 20 | "Cast": "Humphrey Bogart, Ingrid Bergman, Paul Henreid", 21 | "Director": "Michael Curtiz" 22 | }, 23 | { 24 | "Rank": 4, 25 | "Movie": "Raging Bull", 26 | "Year": 1980, 27 | "Cast": "Robert De Niro", 28 | "Director": "Martin Scorsese" 29 | }, 30 | { 31 | "Rank": 5, 32 | "Movie": "Singin' In The Rain", 33 | "Year": 1952, 34 | "Cast": "Gene Kelly, Donald O'Connor, Debbie Reynolds", 35 | "Director": "Gene Kelly" 36 | }, 37 | { 38 | "Rank": 6, 39 | "Movie": "Gone With The Wind", 40 | "Year": 1940, 41 | "Cast": "Vivien Leigh, Clark Gable", 42 | "Director": "Victor Fleming" 43 | }, 44 | { 45 | "Rank": 7, 46 | "Movie": "Lawrence Of Arabia", 47 | "Year": 1962, 48 | "Cast": "Peter O'Toole, Alec Guinness, Anthony Quinn", 49 | "Director": "David Lean" 50 | }, 51 | { 52 | "Rank": 8, 53 | "Movie": "Schindler's List", 54 | "Year": 1993, 55 | "Cast": "Liam Neeson, Ben Kingsley, Ralph Fiennes", 56 | "Director": "Steven Spielberg" 57 | }, 58 | { 59 | "Rank": 9, 60 | "Movie": "Vertigo", 61 | "Year": 1958, 62 | "Cast": "James Stewart, Kim Novak", 63 | "Director": "Alfred Hitchcock" 64 | }, 65 | { 66 | "Rank": 10, 67 | "Movie": "The Wizard Of Oz", 68 | "Year": 1939, 69 | "Cast": "Judy Garland, Frank Morgan, Ray Bolger, Jack Haley, Bert Lahr", 70 | "Director": "Victor Fleming" 71 | }, 72 | { 73 | "Rank": 11, 74 | "Movie": "City Lights", 75 | "Year": 1931, 76 | "Cast": "Charlie Chaplin", 77 | "Director": "Charlie Chaplin" 78 | }, 79 | { 80 | "Rank": 12, 81 | "Movie": "The Searchers", 82 | "Year": 1956, 83 | "Cast": "John Wayne", 84 | "Director": "John Ford" 85 | }, 86 | { 87 | "Rank": 13, 88 | "Movie": "Star Wars", 89 | "Year": 1977, 90 | "Cast": "Mark Hamill, Harrison Ford, Carrie Fisher", 91 | "Director": "George Lucas" 92 | }, 93 | { 94 | "Rank": 14, 95 | "Movie": "Psycho", 96 | "Year": 1960, 97 | "Cast": "Anthony Perkins, Janet Leigh", 98 | "Director": "Alfred Hitchcock" 99 | }, 100 | { 101 | "Rank": 15, 102 | "Movie": "2001: A Space Odyssey", 103 | "Year": 1968, 104 | "Cast": "Keir Dullea, Gary Lockwood", 105 | "Director": "Stanley Kubrick" 106 | }, 107 | { 108 | "Rank": 16, 109 | "Movie": "Sunset Blvd.", 110 | "Year": 1950, 111 | "Cast": "William Holden, Gloria Swanson", 112 | "Director": "Billy Wilder" 113 | }, 114 | { 115 | "Rank": 17, 116 | "Movie": "The Graduate", 117 | "Year": 1967, 118 | "Cast": "Dustin Hoffman, Anne Bancroft", 119 | "Director": "Mike Nichols" 120 | }, 121 | { 122 | "Rank": 18, 123 | "Movie": "The General", 124 | "Year": 1927, 125 | "Cast": "Buster Keaton", 126 | "Director": "Buster Keaton" 127 | }, 128 | { 129 | "Rank": 19, 130 | "Movie": "On The Waterfront", 131 | "Year": 1954, 132 | "Cast": "Marlon Brando", 133 | "Director": "Elia Kazan" 134 | }, 135 | { 136 | "Rank": 20, 137 | "Movie": "It's A Wonderful Life", 138 | "Year": 1946, 139 | "Cast": "James Steward, Donna Reed", 140 | "Director": "Frank Capra" 141 | }, 142 | { 143 | "Rank": 21, 144 | "Movie": "Chinatown", 145 | "Year": 1974, 146 | "Cast": "Jack Nicholson, Faye Dunaway", 147 | "Director": "Roman Polanski" 148 | }, 149 | { 150 | "Rank": 22, 151 | "Movie": "Some Like It Hot", 152 | "Year": 1959, 153 | "Cast": "Marylin Monroe, Tony Curtis, Jack Lemmon", 154 | "Director": "Billy Wilder" 155 | }, 156 | { 157 | "Rank": 23, 158 | "Movie": "The Grapes Of Wrath", 159 | "Year": 1940, 160 | "Cast": "Henry Fonda", 161 | "Director": "John Ford" 162 | }, 163 | { 164 | "Rank": 24, 165 | "Movie": "E.T. The Extra-Terrestrial", 166 | "Year": 1982, 167 | "Cast": "Henry Thomas, Drew Barrymore, Peter Coyote, Dee Wallace", 168 | "Director": "Steven Spielberg" 169 | }, 170 | { 171 | "Rank": 25, 172 | "Movie": "To Kill A Mockingbird", 173 | "Year": 1962, 174 | "Cast": "Gregory Peck", 175 | "Director": "Robert Mulligan" 176 | }, 177 | { 178 | "Rank": 26, 179 | "Movie": "Mr. Smith Goes To Washington", 180 | "Year": 1939, 181 | "Cast": "James Stewart", 182 | "Director": "Frank Capra" 183 | }, 184 | { 185 | "Rank": 27, 186 | "Movie": "High Noon", 187 | "Year": 1952, 188 | "Cast": "Gary Cooper", 189 | "Director": "Fred Zinnemann" 190 | }, 191 | { 192 | "Rank": 28, 193 | "Movie": "All About Eve", 194 | "Year": 1950, 195 | "Cast": "Bette Davis, Anne Baxter, George Sanders, Celeste Holm", 196 | "Director": "Joseph L. Mankiewicz" 197 | }, 198 | { 199 | "Rank": 29, 200 | "Movie": "Double Indemnity", 201 | "Year": 1944, 202 | "Cast": "Fred MacMurray, Barbara Stanwyck, Edward G. Robinson", 203 | "Director": "Billy Wilder" 204 | }, 205 | { 206 | "Rank": 30, 207 | "Movie": "Apocalypse Now", 208 | "Year": 1979, 209 | "Cast": "Marlon Brando, Robert Duvall, Martin Sheen", 210 | "Director": "Francis Coppola" 211 | }, 212 | { 213 | "Rank": 31, 214 | "Movie": "The Maltese Falcon", 215 | "Year": 1941, 216 | "Cast": "Humphrey Bogart, Mary Astor", 217 | "Director": "John Huston" 218 | }, 219 | { 220 | "Rank": 32, 221 | "Movie": "The Godfather Part II", 222 | "Year": 1974, 223 | "Cast": "Al Pacino, Robert Duvall, Diane Keaton, Robert De Niro", 224 | "Director": "Francis Ford Coppola" 225 | }, 226 | { 227 | "Rank": 33, 228 | "Movie": "One Flew Over The Cuckoo's Nest", 229 | "Year": 1975, 230 | "Cast": "Jack Nicholson, Louise Fletcher, Danny DeVito", 231 | "Director": "Milos Forman" 232 | }, 233 | { 234 | "Rank": 34, 235 | "Movie": "Snow White And The Seven Dwarfs", 236 | "Year": 1938, 237 | "Cast": "Adriana Caselotti, Lucille LaVerne", 238 | "Director": "David Hand" 239 | }, 240 | { 241 | "Rank": 35, 242 | "Movie": "Annie Hall", 243 | "Year": 1977, 244 | "Cast": "Woody Allen, Diane Keaton", 245 | "Director": "Woody Allen" 246 | }, 247 | { 248 | "Rank": 36, 249 | "Movie": "The Bridge On The River Kwai", 250 | "Year": 1957, 251 | "Cast": "William Holden, Alec Guinness, Jack Hawkins", 252 | "Director": "David Lean" 253 | }, 254 | { 255 | "Rank": 37, 256 | "Movie": "The Best Year Of Our Lives", 257 | "Year": 1946, 258 | "Cast": "Myrna Loy, Fredric March, Dana Andrews", 259 | "Director": "William Wyler" 260 | }, 261 | { 262 | "Rank": 38, 263 | "Movie": "The Treasure Of The Sierra Madre", 264 | "Year": 1948, 265 | "Cast": "Humphrey Bogart, Walter Huston, Tim Holt", 266 | "Director": "John Huston" 267 | }, 268 | { 269 | "Rank": 39, 270 | "Movie": "Dr. Strangelove", 271 | "Year": 1964, 272 | "Cast": "Peter Sellers, George C. Scott", 273 | "Director": "Stanley Kubrick" 274 | }, 275 | { 276 | "Rank": 40, 277 | "Movie": "The Sound Of Music", 278 | "Year": 1965, 279 | "Cast": "Julie Andrews, Christopher Plummer, Eleanor Parker", 280 | "Director": "Robert Wise" 281 | }, 282 | { 283 | "Rank": 41, 284 | "Movie": "King Kong", 285 | "Year": 1933, 286 | "Cast": "Fay Wray, Robert Armstrong, Bruce Cabot", 287 | "Director": "Merian C. Cooper" 288 | }, 289 | { 290 | "Rank": 42, 291 | "Movie": "Bonnie And Clyde", 292 | "Year": 1967, 293 | "Cast": "Warren Beatty, Faye Dunaway", 294 | "Director": "Arthur Penn" 295 | }, 296 | { 297 | "Rank": 43, 298 | "Movie": "Midnight Cowboy", 299 | "Year": 1969, 300 | "Cast": "Dustin Hoffman, Jon Voight", 301 | "Director": "John Schlesinger" 302 | }, 303 | { 304 | "Rank": 44, 305 | "Movie": "The Philadelphia Story", 306 | "Year": 1940, 307 | "Cast": "Cary Grant, Katharine Hepburn, James Stewart", 308 | "Director": "George Cukor" 309 | }, 310 | { 311 | "Rank": 45, 312 | "Movie": "Shane", 313 | "Year": 1953, 314 | "Cast": "Alan Ladd, Jean Arthur, Van Heflin", 315 | "Director": "George Stevens" 316 | }, 317 | { 318 | "Rank": 46, 319 | "Movie": "It Happened One Night", 320 | "Year": 1934, 321 | "Cast": "Clark Gable, Claudette Colbert", 322 | "Director": "Frank Capra" 323 | }, 324 | { 325 | "Rank": 47, 326 | "Movie": "A Streetcar Named Desire", 327 | "Year": 1951, 328 | "Cast": "Vivien Leigh, Marlon Brando", 329 | "Director": "Elia Kazan" 330 | }, 331 | { 332 | "Rank": 48, 333 | "Movie": "Rear Window", 334 | "Year": 1954, 335 | "Cast": "James Stewart, Grace Kelly", 336 | "Director": "Alfred Hitchcock" 337 | }, 338 | { 339 | "Rank": 49, 340 | "Movie": "Intolerance", 341 | "Year": 1916, 342 | "Cast": "Vera Lewis, Ralph Lewis, Mae Marsh", 343 | "Director": "D. W. Griffith" 344 | }, 345 | { 346 | "Rank": 50, 347 | "Movie": "The Lord Of The Rings: The Fellowship Of The Ring", 348 | "Year": 2001, 349 | "Cast": "Elijah Wood, Ian McKellen, Viggo Mortensen", 350 | "Director": "Peter Jackson" 351 | }, 352 | { 353 | "Rank": 51, 354 | "Movie": "West Side Story", 355 | "Year": 1961, 356 | "Cast": "Natalie Wood, Richard Beymer", 357 | "Director": "Robert Wise" 358 | }, 359 | { 360 | "Rank": 52, 361 | "Movie": "Taxi Driver", 362 | "Year": 1976, 363 | "Cast": "Robert De Niro, Jodie Foster", 364 | "Director": "Martin Scorsese" 365 | }, 366 | { 367 | "Rank": 53, 368 | "Movie": "The Deer Hunter", 369 | "Year": 1978, 370 | "Cast": "Robert De Niro, Meryl Streep", 371 | "Director": "Michael Cimino" 372 | }, 373 | { 374 | "Rank": 54, 375 | "Movie": "M*A*S*H", 376 | "Year": 1970, 377 | "Cast": "Donald Sutherland, Elliott Gould, Tom Skerritt, Sally Kellerman, Robert Duvall", 378 | "Director": "Robert Altman" 379 | }, 380 | { 381 | "Rank": 55, 382 | "Movie": "North By Northwest", 383 | "Year": 1959, 384 | "Cast": "Cary Grant, Eva Marie Saint, James Mason", 385 | "Director": "Alfred Hitchcock" 386 | }, 387 | { 388 | "Rank": 56, 389 | "Movie": "Jaws", 390 | "Year": 1975, 391 | "Cast": "Roy Scheider, Robert Shaw, Richard Dreyfuss", 392 | "Director": "Steven Spielberg" 393 | }, 394 | { 395 | "Rank": 57, 396 | "Movie": "Rocky", 397 | "Year": 1976, 398 | "Cast": "Sylvester Stallone", 399 | "Director": "John G. Avildsen" 400 | }, 401 | { 402 | "Rank": 58, 403 | "Movie": "The Gold Rush", 404 | "Year": 1925, 405 | "Cast": "Charlie Chaplin", 406 | "Director": "Charlie Chaplin" 407 | }, 408 | { 409 | "Rank": 59, 410 | "Movie": "Nashville", 411 | "Year": 1975, 412 | "Cast": "David Arkin, Barbara Baxley, Ned Beatty", 413 | "Director": "Robert Altman" 414 | }, 415 | { 416 | "Rank": 60, 417 | "Movie": "Duck Soup", 418 | "Year": 1933, 419 | "Cast": "Groucho Marx, Harpo Marx, Chico Marx, Zeppo Marx", 420 | "Director": "Leo McCarey" 421 | }, 422 | { 423 | "Rank": 61, 424 | "Movie": "Sullivan's Travels", 425 | "Year": 1941, 426 | "Cast": "Joel McCrea, Veronica Lake, Robert Warwick", 427 | "Director": "Preston Sturges" 428 | }, 429 | { 430 | "Rank": 62, 431 | "Movie": "American Graffiti", 432 | "Year": 1973, 433 | "Cast": "Richard Dreyfuss, Ronny Howard, Paul Le Mat", 434 | "Director": "George Lucas" 435 | }, 436 | { 437 | "Rank": 63, 438 | "Movie": "Cabaret", 439 | "Year": 1972, 440 | "Cast": "Liza Minnelli, Michael York, Helmut Griem", 441 | "Director": "Bob Fosse" 442 | }, 443 | { 444 | "Rank": 64, 445 | "Movie": "Network", 446 | "Year": 1976, 447 | "Cast": "Faye Dunaway, William Holden, Peter Finch, Robert Duvall", 448 | "Director": "Sydney Lumet" 449 | }, 450 | { 451 | "Rank": 65, 452 | "Movie": "The African Queen", 453 | "Year": 1951, 454 | "Cast": "Humphrey Bogart, Katharine Hepburn", 455 | "Director": "John Huston" 456 | }, 457 | { 458 | "Rank": 66, 459 | "Movie": "Raiders Of The Lost Ark", 460 | "Year": 1981, 461 | "Cast": "Harrison Ford, Karen Allen, Paul Freeman", 462 | "Director": "Steven Spielberg" 463 | }, 464 | { 465 | "Rank": 67, 466 | "Movie": "Who's Afraid Of Virginia Woolf?", 467 | "Year": 1966, 468 | "Cast": "Elizabeth Taylor, Richard Burton", 469 | "Director": "Mike Nichols" 470 | }, 471 | { 472 | "Rank": 68, 473 | "Movie": "Unforgiven", 474 | "Year": 1992, 475 | "Cast": "Clint Eastwood, Gene Hackman, Morgan Freeman", 476 | "Director": "Clint Eastwood" 477 | }, 478 | { 479 | "Rank": 69, 480 | "Movie": "Tootsie", 481 | "Year": 1982, 482 | "Cast": "Dustin Hoffman, Jessica Lange", 483 | "Director": "Sydney Pollack" 484 | }, 485 | { 486 | "Rank": 70, 487 | "Movie": "A Clockwork Orange", 488 | "Year": 1971, 489 | "Cast": "Malcolm McDowell, Patrick Magee", 490 | "Director": "Stanley Kubrick" 491 | }, 492 | { 493 | "Rank": 71, 494 | "Movie": "Saving Private Ryan", 495 | "Year": 1998, 496 | "Cast": "Tom Hanks, Matt Damon, Tom Sizemore, Jeremy Davies", 497 | "Director": "Steven Spielberg" 498 | }, 499 | { 500 | "Rank": 72, 501 | "Movie": "The Shawshank Redemption", 502 | "Year": 1994, 503 | "Cast": "Tim Robbins, Morgan Freeman", 504 | "Director": "Frank Darabont" 505 | }, 506 | { 507 | "Rank": 73, 508 | "Movie": "Butch Cassidy And The Sundance Kid", 509 | "Year": 1969, 510 | "Cast": "Paul Newman, Robert Redford, Katharine Ross", 511 | "Director": "George Roy Hill" 512 | }, 513 | { 514 | "Rank": 74, 515 | "Movie": "The Silence Of The Lambs", 516 | "Year": 1991, 517 | "Cast": "Jodie Foster, Anthony Hopkins", 518 | "Director": "Jonathan Demme" 519 | }, 520 | { 521 | "Rank": 75, 522 | "Movie": "In The Heat Of The Night", 523 | "Year": 1967, 524 | "Cast": "Sydney Poitier, Rod Steiger", 525 | "Director": "Norman Jewison" 526 | }, 527 | { 528 | "Rank": 76, 529 | "Movie": "Forrest Gump", 530 | "Year": 1994, 531 | "Cast": "Tom Hanks, Robin Wright, Gary Sinise", 532 | "Director": "Robert Zemeckis" 533 | }, 534 | { 535 | "Rank": 77, 536 | "Movie": "All The President's Men", 537 | "Year": 1976, 538 | "Cast": "Dustin Hoffman, Robert Redford", 539 | "Director": "Alan J. Pakula" 540 | }, 541 | { 542 | "Rank": 78, 543 | "Movie": "Modern Times", 544 | "Year": 1936, 545 | "Cast": "Charlie Chaplin", 546 | "Director": "Charlie Chaplin" 547 | }, 548 | { 549 | "Rank": 79, 550 | "Movie": "The Wild Bunch", 551 | "Year": 1969, 552 | "Cast": "William Holden, Ernest Borgnine", 553 | "Director": "Sam Peckinpah" 554 | }, 555 | { 556 | "Rank": 80, 557 | "Movie": "The Apartment", 558 | "Year": 1960, 559 | "Cast": "Jack Lemmon, Shirley McLaine, Fred MacMurray", 560 | "Director": "Billy Wilder" 561 | }, 562 | { 563 | "Rank": 81, 564 | "Movie": "Spartacus", 565 | "Year": 1960, 566 | "Cast": "Kirk Douglas, Laurence Olivier, Jean Simmons, Charles Laughton, Peter Ustinov, John Gavin", 567 | "Director": "Stanley Kubrick" 568 | }, 569 | { 570 | "Rank": 82, 571 | "Movie": "Sunrise", 572 | "Year": 1927, 573 | "Cast": "George O'Brien, Janet Gaynor, Margaret Livingston", 574 | "Director": "F. W. Murnau" 575 | }, 576 | { 577 | "Rank": 83, 578 | "Movie": "Titanic", 579 | "Year": 1997, 580 | "Cast": "Leonardo DiCaprio, Kate Winslet, Billy Zane", 581 | "Director": "James Cameron" 582 | }, 583 | { 584 | "Rank": 84, 585 | "Movie": "Easy Rider", 586 | "Year": 1969, 587 | "Cast": "Peter Fonda, Dennis Hopper", 588 | "Director": "Dennis Hopper" 589 | }, 590 | { 591 | "Rank": 85, 592 | "Movie": "A Night At The Opera", 593 | "Year": 1935, 594 | "Cast": "Groucho Marx, Chico Marx, Harpo Marx", 595 | "Director": "Sam Wood" 596 | }, 597 | { 598 | "Rank": 86, 599 | "Movie": "Platoon", 600 | "Year": 1986, 601 | "Cast": "Tom Berenger, Willem Dafoe, Charlie Sheen", 602 | "Director": "Oliver Stone" 603 | }, 604 | { 605 | "Rank": 87, 606 | "Movie": "12 Angry Men", 607 | "Year": 1957, 608 | "Cast": "Henry Fonda, Lee J. Cobb, Ed Begley", 609 | "Director": "Sydney Lumet" 610 | }, 611 | { 612 | "Rank": 88, 613 | "Movie": "Bringing Up Baby", 614 | "Year": 1938, 615 | "Cast": "Katharine Hepburn, Cary Grant", 616 | "Director": "Howard Hawks" 617 | }, 618 | { 619 | "Rank": 89, 620 | "Movie": "The Sixth Sense", 621 | "Year": 1999, 622 | "Cast": "Bruce Willis, Haley Joel Osment, Toni Collette", 623 | "Director": "M. Night Shyamalan" 624 | }, 625 | { 626 | "Rank": 90, 627 | "Movie": "Swing Time", 628 | "Year": 1936, 629 | "Cast": "Fred Astaire, Ginger Rogers", 630 | "Director": "George Stevens" 631 | }, 632 | { 633 | "Rank": 91, 634 | "Movie": "Sophie's Choice", 635 | "Year": 1982, 636 | "Cast": "Meryl Streep, Kevin Kline, Peter MacNicol", 637 | "Director": "Alan J. Pakula" 638 | }, 639 | { 640 | "Rank": 92, 641 | "Movie": "Goodfellas", 642 | "Year": 1990, 643 | "Cast": "Robert De Niro, Ray Liotta, Joe Pesci, Lorraine Bracco, Paul Sorvino", 644 | "Director": "Martin Scorsese" 645 | }, 646 | { 647 | "Rank": 93, 648 | "Movie": "The French Connection", 649 | "Year": 1971, 650 | "Cast": "Gene Hackman, Fernando Rey, Roy Scheider", 651 | "Director": "William Friedkin" 652 | }, 653 | { 654 | "Rank": 94, 655 | "Movie": "Pulp Fiction", 656 | "Year": 1994, 657 | "Cast": "John Travolta, Samuel L. Jackson", 658 | "Director": "Quentin Tarantino" 659 | }, 660 | { 661 | "Rank": 95, 662 | "Movie": "The Last Picture Show", 663 | "Year": 1971, 664 | "Cast": "Timothy Bottoms, Jeff Bridges, Cybill Shepherd", 665 | "Director": "Peter Bogdanovich" 666 | }, 667 | { 668 | "Rank": 96, 669 | "Movie": "Do The Right Thing", 670 | "Year": 1989, 671 | "Cast": "Danny Aiello, Ossie Davis, Ruby Dee", 672 | "Director": "Spike Lee" 673 | }, 674 | { 675 | "Rank": 97, 676 | "Movie": "Blade Runner", 677 | "Year": 1982, 678 | "Cast": "Harrison Ford, Sean Young", 679 | "Director": "Ridley Scott" 680 | }, 681 | { 682 | "Rank": 98, 683 | "Movie": "Yankee Doodle Dandy", 684 | "Year": 1942, 685 | "Cast": "James Cagney, Joan Leslie", 686 | "Director": "Michael Curtiz" 687 | }, 688 | { 689 | "Rank": 99, 690 | "Movie": "Toy Story", 691 | "Year": 1995, 692 | "Cast": "Tom Hanks, Tim Allen", 693 | "Director": "John Lasseter" 694 | }, 695 | { 696 | "Rank": 100, 697 | "Movie": "Ben-Hur", 698 | "Year": 1959, 699 | "Cast": "Charlton Heston, Jack Hawkins", 700 | "Director": "William Wyler" 701 | } 702 | ] -------------------------------------------------------------------------------- /server/AFITop100ListVM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using DotNetify; 5 | using DotNetify.Security; 6 | 7 | namespace server 8 | { 9 | [Authorize] 10 | public class AFITop100ListVM : BaseVM 11 | { 12 | private readonly MovieService _movieService; 13 | private int _recordsPerPage; 14 | 15 | public int RecordsPerPage 16 | { 17 | set 18 | { 19 | _recordsPerPage = value; 20 | MaxPage = (int)Math.Ceiling(_movieService.GetAFITop100().Count() / (double)_recordsPerPage); 21 | } 22 | } 23 | 24 | public int MaxPage 25 | { 26 | get { return Get(); } 27 | set { Set(value); } 28 | } 29 | 30 | public int CurrentPage 31 | { 32 | get { return Get(); } 33 | set { Set(value); } 34 | } 35 | 36 | public Action Next => () => 37 | { 38 | if (CurrentPage < MaxPage) 39 | { 40 | CurrentPage++; 41 | Changed(nameof(Movies)); 42 | } 43 | }; 44 | 45 | public IEnumerable Movies => _movieService 46 | .GetAFITop100() 47 | .Select(movie => new { Rank = movie.Rank, Movie = movie.Movie }) 48 | .Skip(_recordsPerPage * (CurrentPage - 1)) 49 | .Take(_recordsPerPage); 50 | 51 | public AFITop100ListVM(MovieService movieService) 52 | { 53 | _movieService = movieService; 54 | RecordsPerPage = 10; 55 | CurrentPage = 1; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/AuthServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Principal; 4 | using System.Text; 5 | using System.IdentityModel.Tokens.Jwt; 6 | using System.Security.Claims; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.Extensions.Options; 11 | using Microsoft.IdentityModel.Tokens; 12 | using Newtonsoft.Json; 13 | 14 | namespace server 15 | { 16 | public static class AppBuilderExtension 17 | { 18 | /// 19 | /// Provides end point that generates authentication tokens. 20 | /// 21 | /// 22 | public static void UseAuthServer(this IApplicationBuilder app) 23 | { 24 | string secretKey = "dotnetifydemo_secretkey_123!"; 25 | var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)); 26 | 27 | app.UseMiddleware(Options.Create(new TokenProviderOptions 28 | { 29 | Audience = "DotNetifyDemoApp", 30 | Issuer = "DotNetifyDemoServer", 31 | SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256), 32 | })); 33 | } 34 | } 35 | 36 | public class TokenProviderOptions 37 | { 38 | public string Path { get; set; } = "/token"; 39 | public string Issuer { get; set; } 40 | public string Audience { get; set; } 41 | public TimeSpan Expiration { get; set; } = TimeSpan.FromSeconds(120); 42 | public SigningCredentials SigningCredentials { get; set; } 43 | } 44 | 45 | /// 46 | /// Middleware for generating authentication tokens. 47 | /// Reference: https://stormpath.com/blog/token-authentication-asp-net-core 48 | /// 49 | public class AuthServerMiddleware 50 | { 51 | private readonly RequestDelegate _next; 52 | private readonly TokenProviderOptions _options; 53 | 54 | public AuthServerMiddleware(RequestDelegate next, IOptions options) 55 | { 56 | _next = next; 57 | _options = options.Value; 58 | } 59 | 60 | public Task Invoke(HttpContext context) 61 | { 62 | // If the request path doesn't match, skip 63 | if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal)) 64 | return _next(context); 65 | 66 | // Request must be POST with Content-Type: application/x-www-form-urlencoded 67 | if (!context.Request.Method.Equals("POST") || !context.Request.HasFormContentType) 68 | { 69 | context.Response.StatusCode = 400; 70 | return context.Response.WriteAsync("Bad request."); 71 | } 72 | 73 | return GenerateToken(context); 74 | } 75 | 76 | private async Task GenerateToken(HttpContext context) 77 | { 78 | var username = context.Request.Form["username"]; 79 | var password = context.Request.Form["password"]; 80 | 81 | var identity = await GetIdentity(username, password); 82 | if (identity == null) 83 | { 84 | context.Response.StatusCode = 401; 85 | await context.Response.WriteAsync("Invalid username or password."); 86 | return; 87 | } 88 | 89 | var now = DateTimeOffset.Now; 90 | 91 | // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims. 92 | // You can add other claims here, if you want: 93 | var claims = new List 94 | { 95 | new Claim(JwtRegisteredClaimNames.Sub, username), 96 | new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), 97 | new Claim(JwtRegisteredClaimNames.Iat, now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) 98 | }; 99 | 100 | claims.AddRange(identity.Claims); 101 | 102 | // Create the JWT and write it to a string 103 | var jwt = new JwtSecurityToken( 104 | issuer: _options.Issuer, 105 | audience: _options.Audience, 106 | claims: claims, 107 | notBefore: now.DateTime, 108 | expires: now.DateTime.Add(_options.Expiration), 109 | signingCredentials: _options.SigningCredentials); 110 | var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); 111 | 112 | var response = new 113 | { 114 | access_token = encodedJwt, 115 | expires_in = (int)_options.Expiration.TotalSeconds 116 | }; 117 | 118 | // Serialize and return the response 119 | context.Response.ContentType = "application/json"; 120 | await context.Response.WriteAsync(JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented })); 121 | } 122 | 123 | private Task GetIdentity(string username, string password) 124 | { 125 | ClaimsIdentity identity = null; 126 | 127 | if (username == "guest" && password == "dotnetify") 128 | { 129 | identity = new ClaimsIdentity(new GenericIdentity(username, "Token"), new Claim[] { 130 | new Claim(ClaimsIdentity.DefaultNameClaimType, username) 131 | }); 132 | } 133 | return Task.FromResult(identity); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /server/LiveGaugeVM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using DotNetify; 4 | using DotNetify.Security; 5 | 6 | namespace server 7 | { 8 | [Authorize] 9 | public class LiveGaugeVM : BaseVM 10 | { 11 | private const int _timeInterval = 1000; 12 | private Timer _timer; 13 | 14 | public int Value { get; set; } 15 | 16 | public LiveGaugeVM() 17 | { 18 | var random = new Random(); 19 | _timer = new Timer(state => 20 | { 21 | Value = random.Next(1, 100); 22 | 23 | Changed(nameof(Value)); 24 | PushUpdates(); 25 | 26 | }, null, _timeInterval, _timeInterval); 27 | } 28 | 29 | public override void Dispose() 30 | { 31 | _timer.Dispose(); 32 | base.Dispose(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/MovieService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Newtonsoft.Json; 4 | using Microsoft.Extensions.FileProviders; 5 | 6 | namespace server 7 | { 8 | public class MovieRecord 9 | { 10 | public int Rank { get; set; } 11 | public string Movie { get; set; } 12 | public int Year { get; set; } 13 | public string Cast { get; set; } 14 | public string Director { get; set; } 15 | } 16 | 17 | public class MovieService 18 | { 19 | private readonly IFileProvider _fileProvider; 20 | 21 | public MovieService(IFileProvider fileProvider) 22 | { 23 | _fileProvider = fileProvider; 24 | } 25 | 26 | public IEnumerable GetAFITop100() 27 | { 28 | var jsonFile = _fileProvider.GetFileInfo("AFITop100.json"); 29 | using (var stream = jsonFile.CreateReadStream()) 30 | using (var reader = new System.IO.StreamReader(stream)) 31 | { 32 | string data = reader.ReadToEnd(); 33 | return JsonConvert.DeserializeObject>(data); 34 | } 35 | } 36 | 37 | public MovieRecord GetMovieByAFIRank(int rank) => GetAFITop100().FirstOrDefault(i => i.Rank == rank); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace server 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "server": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "http://169.254.80.80:5000/" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /server/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.FileProviders; 9 | using Microsoft.IdentityModel.Tokens; 10 | using DotNetify; 11 | using DotNetify.Security; 12 | 13 | namespace server 14 | { 15 | public class Startup 16 | { 17 | public void ConfigureServices(IServiceCollection services) 18 | { 19 | services.AddSingleton(new PhysicalFileProvider(Directory.GetCurrentDirectory())); 20 | services.AddSingleton(); 21 | 22 | services.AddMemoryCache(); 23 | services.AddSignalR(); 24 | services.AddDotNetify(); 25 | } 26 | public void Configure(IApplicationBuilder app) 27 | { 28 | app.UseWebSockets(); 29 | app.UseSignalR(routes => routes.MapDotNetifyHub()); 30 | app.UseDotNetify(config => { 31 | string secretKey = "dotnetifydemo_secretkey_123!"; 32 | var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)); 33 | var tokenValidationParameters = new TokenValidationParameters 34 | { 35 | IssuerSigningKey = signingKey, 36 | ValidAudience = "DotNetifyDemoApp", 37 | ValidIssuer = "DotNetifyDemoServer", 38 | ValidateIssuerSigningKey = true, 39 | ValidateAudience = true, 40 | ValidateIssuer = true, 41 | ValidateLifetime = true, 42 | ClockSkew = TimeSpan.FromSeconds(0) 43 | }; 44 | 45 | config.UseDeveloperLogging(); 46 | config.UseJwtBearerAuthentication(tokenValidationParameters); 47 | config.UseFilter(); 48 | }); 49 | 50 | app.UseAuthServer(); 51 | app.Run(async (context) => 52 | await context.Response.WriteAsync("dotnetify-react-native demo server") 53 | ); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /server/server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.1 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/AppNavigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform } from 'react-native'; 3 | import { Ionicons } from '@expo/vector-icons'; 4 | import { StackNavigator, TabNavigator, TabBarBottom } from 'react-navigation'; 5 | 6 | import LoginScreen from './screens/LoginScreen'; 7 | import LiveGaugeScreen from './screens/LiveGaugeScreen'; 8 | import AFITop100Screen from './screens/AFITop100Screen'; 9 | import AFIDetailsScreen from './screens/AFIDetailsScreen'; 10 | 11 | const tabNavigation = TabNavigator( 12 | { 13 | AFITop100: { screen: AFITop100Screen }, 14 | LiveGauge: { screen: LiveGaugeScreen } 15 | }, 16 | { 17 | navigationOptions: ({ navigation }) => ({ 18 | tabBarIcon: ({ focused }) => { 19 | let iconName; 20 | const isIos = Platform.OS === 'ios'; 21 | const iosIconSuffix = focused ? '' : '-outline'; 22 | 23 | switch (navigation.state.routeName) { 24 | case 'LiveGauge': 25 | iconName = isIos ? `ios-speedometer${iosIconSuffix}` : 'md-speedometer'; 26 | break; 27 | case 'AFITop100': 28 | iconName = isIos ? `ios-list-box${iosIconSuffix}` : 'md-list-box'; 29 | break; 30 | } 31 | return ; 32 | } 33 | }), 34 | tabBarOptions: { activeTintColor: '#7ebc3c' }, 35 | tabBarComponent: TabBarBottom, 36 | tabBarPosition: 'bottom', 37 | swipeEnabled: false 38 | } 39 | ); 40 | 41 | export default StackNavigator({ 42 | Login: { screen: LoginScreen }, 43 | Main: { screen: tabNavigation }, 44 | AFIDetails: { 45 | screen: AFIDetailsScreen, 46 | navigationOptions: ({ navigation }) => ({ title: `${navigation.state.params.title}` }) 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /src/Authentication.js: -------------------------------------------------------------------------------- 1 | import { AsyncStorage } from "react-native"; 2 | 3 | class Authentication { 4 | url = ""; 5 | 6 | signIn(username, password) { 7 | return fetch(this.url, { 8 | method: 'post', 9 | mode: 'no-cors', 10 | body: "username=" + username + "&password=" + password, 11 | headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' } 12 | }) 13 | .then(response => { 14 | if (!response.ok) throw new Error(response.status); 15 | return response.json(); 16 | }) 17 | .then(token => { 18 | AsyncStorage.setItem("access_token", token.access_token); 19 | }); 20 | } 21 | 22 | signOut() { 23 | AsyncStorage.removeItem("access_token"); 24 | } 25 | 26 | getAccessToken() { 27 | return AsyncStorage.getItem("access_token"); 28 | } 29 | } 30 | 31 | export default new Authentication(); -------------------------------------------------------------------------------- /src/ScreenTracker.js: -------------------------------------------------------------------------------- 1 | import dotnetify from 'dotnetify/react-native'; 2 | 3 | class ScreenTracker { 4 | screen = ''; 5 | handlers = []; 6 | 7 | reset() { 8 | this.screen = ''; 9 | this.handlers = []; 10 | } 11 | 12 | subscribe(handler) { 13 | this.handlers.push(handler); 14 | return handler; 15 | } 16 | 17 | unsubscribe(handler) { 18 | this.handlers = this.handlers.filter(item => item != handler); 19 | } 20 | 21 | setScreen(navState) { 22 | if (navState) { 23 | this.screen = this.findRoute(navState); 24 | this.handlers.forEach(handler => handler(this.screen)); 25 | } 26 | } 27 | 28 | findRoute = navState => { 29 | return navState.index !== undefined ? this.findRoute(navState.routes[navState.index]) : navState.routeName; 30 | }; 31 | 32 | goToLoginScreen = (navigate, exception) => { 33 | this.reset(); 34 | dotnetify.react.getViewModels().forEach(vm => vm.$destroy()); 35 | navigate('Login', exception); 36 | }; 37 | } 38 | 39 | export default new ScreenTracker(); 40 | -------------------------------------------------------------------------------- /src/screens/AFIDetailsScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { Card } from 'react-native-elements'; 4 | import dotnetify from 'dotnetify/react-native'; 5 | 6 | import ScreenTracker from '../ScreenTracker'; 7 | 8 | export default class AFIDetailsScreen extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.navigate = props.navigation.navigate; 12 | this.state = { MovieDetails: { Cast: '' } }; 13 | 14 | const self = this; 15 | const { rank } = props.navigation.state.params; 16 | this.vm = dotnetify.react.connect('AFIDetailsVM', this, { 17 | vmArg: { Rank: rank }, 18 | exceptionHandler: ex => ScreenTracker.goToLoginScreen(self.navigate, ex) 19 | }); 20 | } 21 | 22 | componentWillUnmount() { 23 | this.vm.$destroy(); 24 | } 25 | 26 | render() { 27 | let item = this.state.MovieDetails; 28 | return ( 29 | 30 | 31 | Year 32 | {item.Year} 33 | Director 34 | {item.Director} 35 | Cast 36 | {item.Cast.split(',').map((cast, idx) => {cast.trim()})} 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | const styles = StyleSheet.create({ 44 | container: { 45 | flex: 1, 46 | backgroundColor: '#ddd' 47 | }, 48 | header: { 49 | fontWeight: 'bold', 50 | marginTop: 10 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /src/screens/AFITop100Screen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator, FlatList, StyleSheet, Text, View } from 'react-native'; 3 | import { ListItem } from 'react-native-elements'; 4 | import dotnetify from 'dotnetify/react-native'; 5 | 6 | import Authentication from '../Authentication'; 7 | import ScreenTracker from '../ScreenTracker'; 8 | 9 | export default class AFITop100Screen extends React.Component { 10 | static navigationOptions = { 11 | title: 'AFI Top 100' 12 | }; 13 | 14 | constructor(props) { 15 | super(props); 16 | this.navigate = props.navigation.navigate; 17 | this.state = { Movies: [] }; 18 | 19 | const self = this; 20 | Authentication.getAccessToken().then( 21 | token => 22 | (this.vm = dotnetify.react.connect('AFITop100ListVM', this, { 23 | vmArg: { RecordsPerPage: 20 }, 24 | setState: this.updateMovies, 25 | headers: { Authorization: 'Bearer ' + token }, 26 | exceptionHandler: ex => ScreenTracker.goToLoginScreen(self.navigate, ex) 27 | })) 28 | ); 29 | } 30 | 31 | componentWillUnmount() { 32 | this.vm.$destroy(); 33 | } 34 | 35 | updateMovies = newState => { 36 | const { Movies } = newState; 37 | newState.Movies = this.state.Movies.concat(Movies); 38 | this.setState(newState); 39 | }; 40 | 41 | getMoreMovies = () => { 42 | if (this.state.CurrentPage < this.state.MaxPage) this.vm.$dispatch({ Next: null }); 43 | }; 44 | 45 | render() { 46 | if (!this.state.Movies.length) 47 | return ( 48 | 49 | 50 | 51 | ); 52 | 53 | return ( 54 | 55 | `${item.Rank}`} 60 | renderItem={({ item }) => { 61 | const navArg = { rank: item.Rank, title: `AFI #${item.Rank}` }; 62 | return ( 63 | 66 | {item.Rank} 67 | {item.Movie} 68 | 69 | } 70 | containerStyle={{ backgroundColor: 'white' }} 71 | onPress={() => this.navigate('AFIDetails', navArg)} 72 | /> 73 | ); 74 | }} 75 | /> 76 | 77 | ); 78 | } 79 | } 80 | 81 | const styles = StyleSheet.create({ 82 | container: { 83 | flex: 1, 84 | backgroundColor: '#fff' 85 | }, 86 | loading: { 87 | flex: 1, 88 | backgroundColor: '#fff', 89 | justifyContent: 'center', 90 | alignItems: 'center' 91 | }, 92 | listItem: { 93 | flexDirection: 'row' 94 | }, 95 | circle: { 96 | width: 25, 97 | height: 25, 98 | borderRadius: 25, 99 | color: 'white', 100 | backgroundColor: '#00BCD4', 101 | textAlign: 'center', 102 | paddingTop: 2, 103 | marginRight: 10, 104 | marginTop: -1 105 | } 106 | }); 107 | -------------------------------------------------------------------------------- /src/screens/LiveGaugeScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'; 3 | import { AnimatedGaugeProgress } from 'react-native-simple-gauge'; 4 | import dotnetify from 'dotnetify/react-native'; 5 | 6 | import ScreenTracker from '../ScreenTracker'; 7 | 8 | const gaugeSize = 250; 9 | const gaugeWidth = 20; 10 | const cropDegree = 45; 11 | const textOffset = gaugeWidth; 12 | const textWidth = gaugeSize - textOffset * 2; 13 | const textHeight = gaugeSize * (1 - cropDegree / 360) - textOffset * 2; 14 | 15 | export default class LiveGaugeScreen extends React.Component { 16 | static navigationOptions = { 17 | title: 'Live Gauge' 18 | }; 19 | 20 | constructor(props) { 21 | super(props); 22 | this.navigate = props.navigation.navigate; 23 | this.state = { Value: null }; 24 | 25 | this.subscription = ScreenTracker.subscribe(this.connectLiveGauge); 26 | } 27 | 28 | componentWillUnmount() { 29 | this.vm && this.vm.$destroy(); 30 | ScreenTracker.unsubscribe(this.subscription); 31 | } 32 | 33 | connectLiveGauge = screen => { 34 | const self = this; 35 | if (screen == 'LiveGauge') { 36 | this.vm = dotnetify.react.connect('LiveGaugeVM', this, { 37 | exceptionHandler: ex => ScreenTracker.goToLoginScreen(self.navigate, ex) 38 | }); 39 | } 40 | else if (this.vm) { 41 | this.vm.$destroy(); 42 | this.vm = null; 43 | } 44 | }; 45 | 46 | render() { 47 | if (!this.state.Value) 48 | return ( 49 | 50 | 51 | 52 | ); 53 | 54 | return ( 55 | 56 | 65 | {fill => ( 66 | 67 | {this.state.Value} 68 | 69 | )} 70 | 71 | 72 | ); 73 | } 74 | } 75 | 76 | const styles = StyleSheet.create({ 77 | container: { 78 | flex: 1, 79 | backgroundColor: '#fff', 80 | justifyContent: 'center', 81 | alignItems: 'center' 82 | }, 83 | digitalView: { 84 | top: textOffset, 85 | left: textOffset, 86 | width: textWidth, 87 | height: textHeight, 88 | position: 'absolute', 89 | alignItems: 'center', 90 | justifyContent: 'center' 91 | }, 92 | digital: { 93 | fontSize: 80 94 | } 95 | }); 96 | -------------------------------------------------------------------------------- /src/screens/LoginScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { Card, Button, FormLabel, FormInput, FormValidationMessage } from 'react-native-elements'; 4 | 5 | import Authentication from '../Authentication'; 6 | 7 | export default class LoginScreen extends React.Component { 8 | static navigationOptions = { 9 | header: null 10 | }; 11 | 12 | constructor(props) { 13 | super(props); 14 | this.navigate = props.navigation.navigate; 15 | this.state = { 16 | exception: this.getNavException(props.navigation.state.params) 17 | }; 18 | } 19 | 20 | login = () => { 21 | let self = this; 22 | this.setState({ validationError: null, exception: null }); 23 | 24 | Authentication.signIn(this.state.user, this.state.password).then(() => self.navigate('Main')).catch(error => { 25 | if (error.message == '401') this.setState({ validationError: 'Invalid user name or password' }); 26 | else this.setState({ exception: error.message }); 27 | }); 28 | }; 29 | 30 | getNavException = ex => { 31 | if (!ex) return null; 32 | return ex.name == 'UnauthorizedAccessException' || ex == 'SecurityTokenExpiredException' ? 'Access expired. Please re-login.' : ex.message; 33 | }; 34 | 35 | render() { 36 | const handleUserInput = user => this.setState({ user: user }); 37 | const handlePasswordInput = pwd => this.setState({ password: pwd }); 38 | return ( 39 | 40 | 41 | 42 | dotNetify 43 | 44 | 45 | {this.state.exception ? {this.state.exception} : null} 46 | User Name 47 | 48 | {this.state.validationError ? {this.state.validationError} : null} 49 | Password 50 | 51 | {this.state.validationError ? {this.state.validationError} : null} 52 |