├── .github ├── weekly-digest.yml └── workflows │ └── review.yml ├── .gitignore ├── .scripts └── update ├── 3box-identities-ed25519 ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── serviceWorker.js │ └── setupTests.js └── tsconfig.json ├── LICENSE ├── README.md ├── bucket-photo-gallery ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.tsx │ ├── Avatar.tsx │ ├── Photos.tsx │ ├── Types.ts │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── serviceWorker.js │ └── setupTests.js └── tsconfig.json ├── hub-browser-auth-app ├── README.md ├── example.env ├── package.json ├── src │ ├── client │ │ ├── index.html │ │ ├── index.ts │ │ ├── simple.html │ │ ├── static │ │ │ └── main.css │ │ ├── tsconfig.json │ │ └── ui.ts │ └── server │ │ ├── api.ts │ │ ├── hub-helpers.ts │ │ ├── index.ts │ │ ├── tsconfig.json │ │ └── wss.ts └── webpack.client.js ├── metamask-identities-ed25519 ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── serviceWorker.js │ └── setupTests.js └── tsconfig.json ├── npm-publish-bucket ├── .gitignore ├── README.md ├── example.env ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js ├── react-native-hub-app ├── .buckconfig ├── .eslintignore ├── .eslintrc.json ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .watchmanconfig ├── App.js ├── README.md ├── __tests__ │ └── App-test.js ├── android │ ├── app │ │ ├── BUCK │ │ ├── build.gradle │ │ ├── build_defs.bzl │ │ ├── debug.keystore │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── threadsdb_app │ │ │ │ └── ReactNativeFlipper.java │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── threadsdb_app │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app.json ├── babel.config.js ├── example.env ├── index.js ├── ios │ ├── Podfile │ ├── Podfile.lock │ ├── threadsdb_app-tvOS │ │ └── Info.plist │ ├── threadsdb_app-tvOSTests │ │ └── Info.plist │ ├── threadsdb_app.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ ├── threadsdb_app-tvOS.xcscheme │ │ │ └── threadsdb_app.xcscheme │ ├── threadsdb_app.xcworkspace │ │ └── contents.xcworkspacedata │ ├── threadsdb_app │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Base.lproj │ │ │ └── LaunchScreen.xib │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── main.m │ └── threadsdb_appTests │ │ ├── Info.plist │ │ └── threadsdb_appTests.m ├── metro.config.js ├── package.json ├── shim.js ├── src │ ├── checklist.tsx │ ├── helpers.tsx │ └── styles.tsx └── tsconfig.json ├── scripts ├── outdated.sh ├── setup.sh └── update.sh ├── simple-auth ├── .eslintrc.json ├── README.md ├── example.env ├── index.js └── package.json └── user-mailbox-setup ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── serviceWorker.js └── setupTests.js └── tsconfig.json /.github/weekly-digest.yml: -------------------------------------------------------------------------------- 1 | # Configuration for weekly-digest - https://github.com/apps/weekly-digest 2 | publishDay: fri 3 | canPublishIssues: true 4 | canPublishPullRequests: true 5 | canPublishContributors: true 6 | canPublishStargazers: true 7 | canPublishCommits: true 8 | -------------------------------------------------------------------------------- /.github/workflows/review.yml: -------------------------------------------------------------------------------- 1 | name: Review 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | spelling: 9 | name: Spelling 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: reviewdog/action-misspell@v1 14 | with: 15 | reporter: github-pr-review 16 | github_token: ${{ secrets.github_token }} 17 | locale: "US" 18 | library-checks: 19 | name: library-checks 20 | runs-on: ubuntu-latest 21 | if: "!contains(github.event.head_commit.message, 'skip-ci')" 22 | strategy: 23 | matrix: 24 | example: [ 25 | 'hub-browser-auth-app', 26 | 'bucket-photo-gallery', 27 | 'react-native-hub-app', 28 | 'metamask-identities-ed25519', 29 | '3box-identities-ed25519', 30 | 'user-mailbox-setup', 31 | 'simple-auth' 32 | ] 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v1 36 | - name: Setup 37 | uses: actions/setup-node@v1 38 | with: 39 | node-version: 12 40 | - name: Install 41 | working-directory: ${{ matrix.example }} 42 | run: npm install 43 | - name: Show versions 44 | working-directory: ${{ matrix.example }} 45 | run: npx ncu '/^@textile/.*$/' 46 | - name: Check versions 47 | working-directory: ${{ matrix.example }} 48 | run: if [ $(npx ncu '/^@textile/.*$/' | grep '@textile/' | wc -l) == 0 ]; then echo 'ok'; else exit 1; fi; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # VSCode settings 32 | .vscode 33 | 34 | **/package-lock.json 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | ~.env 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | 113 | # Android 114 | 115 | *.keystore 116 | *.jks 117 | android/**/*.settings 118 | android/**/*.project 119 | android/**/*.classpath 120 | example/**/*.settings 121 | example/**/*.project 122 | example/**/*.classpath 123 | 124 | # OSX 125 | # 126 | .DS_Store 127 | 128 | .tmp 129 | 130 | # Xcode 131 | # 132 | build/ 133 | *.pbxuser 134 | !default.pbxuser 135 | *.mode1v3 136 | !default.mode1v3 137 | *.mode2v3 138 | !default.mode2v3 139 | *.perspectivev3 140 | !default.perspectivev3 141 | xcuserdata 142 | *.xccheckout 143 | *.moved-aside 144 | DerivedData 145 | *.hmap 146 | *.ipa 147 | *.xcuserstate 148 | project.xcworkspace 149 | 150 | # Xcode Patch 151 | **/xcshareddata/WorkspaceSettings.xcsettings 152 | **/ios/threadsdb_app.xcworkspace/xcshareddata/ 153 | 154 | # Android/IntelliJ 155 | # 156 | build/ 157 | .idea 158 | .gradle 159 | local.properties 160 | *.iml 161 | -------------------------------------------------------------------------------- /.scripts/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeuo pipefail 3 | 4 | npm install -g npm-check-updates 5 | 6 | for d in */ ; do 7 | echo "updating: $d" 8 | (cd $d && npx ncu -u '/^@textile/.*$/' && npm install) 9 | done 10 | 11 | -------------------------------------------------------------------------------- /3box-identities-ed25519/README.md: -------------------------------------------------------------------------------- 1 | See full [README.md here](../README.md). 2 | -------------------------------------------------------------------------------- /3box-identities-ed25519/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3box-textile-identities", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "3box": "^1.21.0", 7 | "@emotion/react": "11.0.0-next.12", 8 | "@textile/hub": "^6.0.1", 9 | "ethers": "^5.0.8", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-fontawesome": "^1.7.1", 13 | "react-scripts": "3.4.1", 14 | "slate-react-system": "^0.1.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^14.0.14", 18 | "@types/react": "^16.9.41", 19 | "@types/react-dom": "^16.9.8", 20 | "@types/react-fontawesome": "^1.6.4", 21 | "http-proxy-middleware": "^1.0.4", 22 | "typescript": "^3.9.5" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "eject": "react-scripts eject", 28 | "textile:check": "npx ncu '/^@textile/.*$/'", 29 | "textile:upgrade": "npx ncu -u '/^@textile/.*$/'" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /3box-identities-ed25519/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/3box-identities-ed25519/public/favicon.ico -------------------------------------------------------------------------------- /3box-identities-ed25519/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /3box-identities-ed25519/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/3box-identities-ed25519/public/logo192.png -------------------------------------------------------------------------------- /3box-identities-ed25519/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/3box-identities-ed25519/public/logo512.png -------------------------------------------------------------------------------- /3box-identities-ed25519/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /3box-identities-ed25519/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /3box-identities-ed25519/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // @ts-ignore 3 | import { dispatchCustomEvent, ButtonPrimary, ButtonDisabled, GlobalNotification } from 'slate-react-system' 4 | import { PrivateKey } from '@textile/hub' 5 | 6 | const Box = require("3box") 7 | 8 | type WindowInstanceWithEthereum = Window & typeof globalThis & { ethereum?: any } 9 | 10 | class App extends React.Component { 11 | state = { 12 | secret: '', 13 | loggedIn: false, 14 | working: false 15 | } 16 | handleChange = (e: any) => this.setState({ [e.target.name]: e.target.value }) 17 | 18 | generatePrivateKey = async (): Promise => { 19 | 20 | if (!(window as WindowInstanceWithEthereum).ethereum) { 21 | throw new Error( 22 | 'Ethereum is not connected. Please download Metamask from https://metamask.io/download.html' 23 | ); 24 | } 25 | 26 | this.setState({working: true}) 27 | 28 | const box = await Box.create((window as WindowInstanceWithEthereum).ethereum) 29 | const [address] = await (window as WindowInstanceWithEthereum).ethereum.enable() 30 | await box.auth([], { address }) 31 | const space = await box.openSpace('io-textile-3box-demo') 32 | await box.syncDone 33 | let identity: PrivateKey 34 | try { 35 | var storedIdent = await space.private.get("ed25519-identity") 36 | if (storedIdent === null) { 37 | throw new Error('No identity') 38 | } 39 | identity = PrivateKey.fromString(storedIdent) 40 | return identity 41 | } catch (e) { 42 | try { 43 | identity = PrivateKey.fromRandom() 44 | const identityString = identity.toString() 45 | await space.private.set("ed25519-identity", identityString) 46 | } catch (err) { 47 | return err.message 48 | } 49 | } 50 | 51 | this.createNotification(identity) 52 | this.setState({loggedIn: true, working: false}) 53 | 54 | return identity 55 | } 56 | 57 | createNotification = (identity: PrivateKey) => { 58 | dispatchCustomEvent({ name: "create-notification", detail: { 59 | id: 1, 60 | description: `PubKey: ${identity.public.toString()}. Your app can now generate and reuse this users PrivateKey for creating user Mailboxes, Threads, and Buckets.`, 61 | timeout: 5000, 62 | }}) 63 | } 64 | 65 | render () { 66 | return ( 67 |
68 | 69 |
70 | {(!this.state.loggedIn && !this.state.working) && 71 | Login with 3Box } 72 | 73 | {(!this.state.loggedIn && this.state.working) && 74 | Connecting... 75 | } 76 | 77 | {(this.state.loggedIn) && 78 | Success! 79 | } 80 |
81 |
82 | ) 83 | } 84 | } 85 | 86 | export default App 87 | -------------------------------------------------------------------------------- /3box-identities-ed25519/src/index.css: -------------------------------------------------------------------------------- 1 | Html, body{ 2 | height:100%; 3 | } 4 | 5 | .container { 6 | display: flex; /* establish flex container */ 7 | flex-direction: column; /* make main axis vertical */ 8 | justify-content: center; /* center items vertically, in this case */ 9 | align-items: center; /* center items horizontally, in this case */ 10 | height: 300px; 11 | } 12 | 13 | .login { 14 | width: 300px; 15 | margin: 5px; 16 | text-align: center; 17 | } 18 | 19 | .login div, .login button, .login input { 20 | margin-top: 10px; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /3box-identities-ed25519/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /3box-identities-ed25519/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /3box-identities-ed25519/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /3box-identities-ed25519/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /3box-identities-ed25519/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /3box-identities-ed25519/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "downlevelIteration": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 textile.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-examples 2 | 3 | All code here is used as reference in the main documentation page, [docs.textile.io](https://docs.textile.io). 4 | 5 | ## Issues & Support 6 | 7 | Please open all issues on [textileio/js-textile](https://github.com/textileio/js-textile/issues) and the name or link to the specific example you are debugging. 8 | 9 | ## Use 10 | 11 | You can find each stand-alone example in each of the subdirectories. 12 | 13 | **Note** 14 | 15 | The chat demo, hub-threaddb-chat, has been temporarily deprecated until we complete the [threads refactor](https://github.com/textileio/js-threads/issues/414) project. 16 | 17 | > Examples and demos using Textile's Javascript/Typescript libraries and clients. 18 | 19 | ### bucket-photo-gallery - setup user buckets to hold files 20 | 21 | This example users non-signing keys for development mode. Here, you'll create a new user and then give them an interface to upload files to their own bucket. 22 | 23 | You can read about key generation here: https://docs.textile.io/hub/apis/. 24 | 25 | Next, you will need to update the example to use your Hub API key, https://github.com/textileio/js-examples/blob/master/bucket-photo-gallery/src/App.tsx#L19. 26 | 27 | Note: you will want to use a signing key if you use this example for a production application. Read more about that process here, https://docs.textile.io/tutorials/hub/production-auth/. 28 | 29 | #### Build & serve 30 | 31 | Change directories into the `bucket-photo-galleries` repo. 32 | 33 | ```bash 34 | npm run start 35 | ``` 36 | 37 | Your browser should automatically launch to the app running on [localhost:3001](http://localhost:3001). 38 | 39 | ### hub-browser-auth-app - shows the full client/server setup to using signed api keys 40 | 41 | This example includes two Typescript projects. A server in `src/server` and a client in `src/client`. The example demonstrates how to use the [Textile Hub](https://docs.textile.io/) APIs from the Browser using user identities and **user group keys**. 42 | 43 | Read the full tutorial accompanying this example on [docs.textile.io](https://docs.textile.io). 44 | 45 | To run, you need to first copy the `example.env` folder to `.env`. Next, you need to update the key and secret fields with values you create using the Hub CLI. 46 | 47 | #### WARNING 48 | 49 | _Do not share any API Key Secrets. This includes User Group Key secret and Account key Secrets. Be sure you never commit them into public repos or share them in published apps._ 50 | 51 | #### Configure 52 | 53 | Create a `.env` file in the root of your project. Ensure you never check this file into your repo or share it, it contains your User Group Key Secret. 54 | 55 | ```bash 56 | cp example.env .env 57 | ``` 58 | 59 | Then replace the `USER_API_KEY` and `USER_API_SECRET` values with those you create using the Textile Hub and your own account or org (see [docs.textile.io](https://docs.textile.io) for details). 60 | 61 | #### Setup 62 | 63 | ```bash 64 | npm install 65 | ``` 66 | 67 | #### Clean 68 | 69 | ```bash 70 | npm run clean 71 | ``` 72 | 73 | #### Run basic auth client 74 | 75 | In this example, you can see how to create a basic user auth flow: 76 | 77 | * the user is defined by a simple keypair created in the browser on demand. 78 | * the user is then granted access to the developer's hub resources through the use of API keys. 79 | * the user can then access their own thread APIs to begin creating threads and buckets. 80 | 81 | The client code is available in `src/basic`. 82 | 83 | #### Build & serve 84 | 85 | You can run the server and client in development mode by opening two terminal windows. 86 | 87 | **Terminal 1: watch the client code** 88 | 89 | ```bash 90 | npm run dev:client 91 | ``` 92 | 93 | **Terminal 2: start the dev server** 94 | 95 | ```bash 96 | npm run start:server 97 | ``` 98 | 99 | You can now view the example at [localhost:3001](http://localhost:3001). 100 | 101 | 102 | 103 | ### user-mailbox-setup - demonstrates how to setup the user mailbox api 104 | 105 | This example uses a hard-coded PrivateKey as your first user's identity. It then uses the User API to enable the user's mailbox. Instead of setting up a second user, it will just send messages from the user to themselves. 106 | 107 | To use this example, you must create a Hub API key and update the code to use it. 108 | 109 | ### react-native-hub-app - demonstrates both bucket and thread functionality in react native 110 | 111 | Like many of the examples, start by copying `example.env` to `.env` and filling our key and secret values from your own Hub account. 112 | 113 | The example, when run, will go through a series of examples that set up user identity, threads, and then buckets with the Textile Hub. 114 | 115 | ### metamask-identities-ed25519 - a password based privatekey generation workflow for textile identities 116 | 117 | This example will use a user's Ethereum address and signing API from Metamask to create a new ed25519 private key identity. It does this in combination with a user supplied password. Your app never needs to store the password or private key, as the user can regenerate them at any time. 118 | 119 | You can use the identity created with any example above, your own apps, and with any Textile API for your user identities. 120 | 121 | ### 3box-identities-ed25519 122 | 123 | Similar to the above example, this example uses 3Box to derive the private key based on the user's ethereum account. It uses the box api to store the private key value on 3Box's server so it can be fetched at a future time. 124 | 125 | You can use the identity created with any example above, your own apps, and with any Textile API for your user identities. 126 | -------------------------------------------------------------------------------- /bucket-photo-gallery/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /bucket-photo-gallery/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run, 6 | 7 | `npm install` 8 | 9 | To launch the example, see the Bucket app section in the main [examples readme](../README.md). -------------------------------------------------------------------------------- /bucket-photo-gallery/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dropzone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@textile/hub": "^6.0.1", 7 | "@types/jdenticon": "^2.2.0", 8 | "browser-image-resizer": "^2.1.0", 9 | "browser-image-size": "^1.1.0", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-dropzone": "^11.0.1", 13 | "react-fontawesome": "^1.7.1", 14 | "react-image-lightbox": "^5.1.1", 15 | "react-jdenticon": "0.0.8", 16 | "react-photo-gallery": "^8.0.0", 17 | "react-scripts": "3.4.1", 18 | "semantic-ui-css": "^2.4.1", 19 | "semantic-ui-react": "^0.88.2" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^14.0.14", 23 | "@types/react": "^16.9.41", 24 | "@types/react-dom": "^16.9.8", 25 | "@types/react-fontawesome": "^1.6.4", 26 | "http-proxy-middleware": "^1.0.4", 27 | "typescript": "^3.9.5" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "react-scripts build", 32 | "eject": "react-scripts eject", 33 | "textile:check": "npx ncu '/^@textile/.*$/'", 34 | "textile:upgrade": "npx ncu -u '/^@textile/.*$/'" 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /bucket-photo-gallery/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/bucket-photo-gallery/public/favicon.ico -------------------------------------------------------------------------------- /bucket-photo-gallery/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /bucket-photo-gallery/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/bucket-photo-gallery/public/logo192.png -------------------------------------------------------------------------------- /bucket-photo-gallery/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/bucket-photo-gallery/public/logo512.png -------------------------------------------------------------------------------- /bucket-photo-gallery/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /bucket-photo-gallery/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /bucket-photo-gallery/src/App.css: -------------------------------------------------------------------------------- 1 | .App .nav { 2 | padding-left: 20px; 3 | padding-right: 20px; 4 | background-color: #333; 5 | justify-content: center; 6 | } 7 | 8 | .App .avatar { 9 | background: none; 10 | border-radius: 5px; 11 | width: 45px; height: 45px; 12 | background: #f3f3f3; 13 | display: flex; 14 | justify-content: center; 15 | align-content: center; 16 | } 17 | 18 | .App button.link { 19 | border-radius: 5px; 20 | height: 45px; 21 | display: block; 22 | align-items: center; 23 | justify-content: center; 24 | background: none; 25 | cursor: pointer; 26 | color: #8f95ff; 27 | font-size: 12px; 28 | outline: 0; 29 | } 30 | 31 | .App .avatar svg { 32 | width: 45px; height: 45px; 33 | } 34 | 35 | .App .rendering { 36 | opacity: 0.1 37 | } 38 | 39 | .App .dropzone-container { 40 | border-radius: 5px; 41 | width: 155px; height: 45px; 42 | display: flex; 43 | align-items: center; 44 | justify-content: space-around; 45 | background: #f3f3f3; 46 | cursor: pointer; 47 | outline: 0; 48 | } 49 | 50 | .App .dropzone-container .dropzone { 51 | outline: 0; 52 | } 53 | .App .dropzone-container .dropzone span { 54 | vertical-align: middle; 55 | margin-right: 10px; 56 | font-size: 10px; 57 | font-weight: 100; 58 | color: #bbb; 59 | } 60 | 61 | .App .dropzone-container:hover { 62 | background: #ddd; 63 | } 64 | 65 | .App .dropzone-container .icon{ 66 | background: none; 67 | color: #8f95ff; 68 | outline: 0; 69 | } 70 | .App .dropzone-container .icon:hover{ 71 | background: none; 72 | } -------------------------------------------------------------------------------- /bucket-photo-gallery/src/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // @ts-ignore 3 | import Jdenticon from 'react-jdenticon'; 4 | import './App.css'; 5 | 6 | export interface AvatarProps { identity: string } 7 | class Avatar extends React.Component{ 8 | 9 | render(){ 10 | return( 11 | 12 | ) 13 | } 14 | } 15 | export default Avatar; 16 | -------------------------------------------------------------------------------- /bucket-photo-gallery/src/Photos.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Gallery, { PhotoProps } from 'react-photo-gallery' 3 | import './App.css'; 4 | 5 | export interface PhotosProps { photos: Array} 6 | class Photos extends React.Component{ 7 | constructor(props: PhotosProps){ 8 | super(props); 9 | this.state = { currentImage: 0 }; 10 | } 11 | 12 | shouldComponentUpdate(nextProps: PhotosProps) { 13 | return this.props.photos.length !== nextProps.photos.length; 14 | } 15 | 16 | render(){ 17 | return( 18 |
19 | 20 |
21 | ) 22 | } 23 | } 24 | export default Photos; 25 | -------------------------------------------------------------------------------- /bucket-photo-gallery/src/Types.ts: -------------------------------------------------------------------------------- 1 | import { Buckets, Identity, UserAuth } from '@textile/hub' 2 | import { PhotoProps } from 'react-photo-gallery' 3 | 4 | export interface PhotoSample { 5 | cid: string 6 | name: string 7 | path: string 8 | width: number 9 | height: number 10 | } 11 | export interface Photo { 12 | date: number 13 | name: string 14 | original: PhotoSample 15 | preview: PhotoSample 16 | thumb: PhotoSample 17 | } 18 | 19 | export interface GalleryIndex { 20 | author: string 21 | date: number 22 | paths: string[] 23 | } 24 | 25 | export interface AppState { 26 | metadata: Array 27 | photos: Array 28 | index: GalleryIndex 29 | isLoading: boolean 30 | isDragActive: boolean 31 | identity?: Identity 32 | userAuth?: UserAuth 33 | buckets?: Buckets 34 | bucketKey?: string 35 | www?: string 36 | url?: string 37 | ipns?: string 38 | } -------------------------------------------------------------------------------- /bucket-photo-gallery/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /bucket-photo-gallery/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /bucket-photo-gallery/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /bucket-photo-gallery/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /bucket-photo-gallery/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /bucket-photo-gallery/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /bucket-photo-gallery/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "downlevelIteration": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | 28 | -------------------------------------------------------------------------------- /hub-browser-auth-app/README.md: -------------------------------------------------------------------------------- 1 | ## Available Scripts 2 | 3 | In the project directory, you can run, 4 | 5 | `npm install` 6 | 7 | To launch the example, see the Auth app section in the main [examples readme](../README.md). -------------------------------------------------------------------------------- /hub-browser-auth-app/example.env: -------------------------------------------------------------------------------- 1 | PORT=3001 2 | SKIP_PREFLIGHT_CHECK=true 3 | 4 | # Create and use secure hub keys (with signing ENABLED) 5 | USER_API_KEY=alkjsdfls 6 | USER_API_SECRET=alkgfjwejfw;ejkf -------------------------------------------------------------------------------- /hub-browser-auth-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rimraf dist", 8 | "start:server": "npm run build:server && run-p watch:server dev:server", 9 | "build:server": "tsc -p src/server/", 10 | "dev:server": "node-dev --respawn dist/server/index.js", 11 | "watch:server": "tsc -p src/server/ -w", 12 | "dev:client": "npm run build:client && webpack --config=webpack.client.js --watch", 13 | "build:client": "webpack --config=webpack.client.js", 14 | "textile:check": "npx ncu '/^@textile/.*$/'", 15 | "textile:upgrade": "npx ncu -u '/^@textile/.*$/'" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "MIT", 20 | "dependencies": { 21 | "@koa/cors": "^3.1.0", 22 | "@textile/hub": "^6.0.1", 23 | "dotenv": "^8.2.0", 24 | "emittery": "^0.7.0", 25 | "jdenticon": "^2.2.0", 26 | "koa": "^2.12.0", 27 | "koa-bodyparser": "^4.3.0", 28 | "koa-json": "^2.0.2", 29 | "koa-logger": "^3.2.1", 30 | "koa-route": "^3.2.0", 31 | "koa-router": "^8.0.8", 32 | "koa-send": "^5.0.0", 33 | "koa-static": "^5.0.0", 34 | "koa-websocket": "^6.0.0" 35 | }, 36 | "devDependencies": { 37 | "@types/koa": "^2.11.3", 38 | "@types/koa-bodyparser": "^4.3.0", 39 | "@types/koa-json": "^2.0.18", 40 | "@types/koa-logger": "^3.1.1", 41 | "@types/koa-router": "^7.4.1", 42 | "@types/lodash": "^4.14.153", 43 | "chai": "^4.2.0", 44 | "chai-http": "^4.3.0", 45 | "copy-webpack-plugin": "^6.0.1", 46 | "expose-loader": "^0.7.5", 47 | "http-server": "^0.12.3", 48 | "lodash": "^4.17.15", 49 | "mocha": "^7.2.0", 50 | "node-dev": "^4.0.0", 51 | "npm-check-updates": "^6.0.1", 52 | "npm-run-all": "^4.1.5", 53 | "rimraf": "^3.0.2", 54 | "ts-loader": "^7.0.5", 55 | "ts-node-dev": "^1.0.0-pre.44", 56 | "tsconfig-paths-webpack-plugin": "^3.2.0", 57 | "typescript": "^3.9.3", 58 | "webpack": "^4.42.0", 59 | "webpack-cli": "^3.3.11" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /hub-browser-auth-app/src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Textile Hub - Token Demo 6 | 7 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 | NO TOKEN 28 |
29 |
30 |
31 | THREADS 32 |
33 | 34 |
35 |
36 |
37 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /hub-browser-auth-app/src/client/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Client, UserAuth, Identity, PrivateKey } from '@textile/hub' 3 | import {displayIdentity, displayStatus, displayAvatar, displayThreadsList} from './ui' 4 | 5 | /** 6 | * Creates a new random keypair-based Identity 7 | * 8 | * The identity will be cached in the browser for later 9 | * sessions. 10 | */ 11 | const getIdentity = (async (): Promise => { 12 | const queryString = window.location.search; 13 | const urlParams = new URLSearchParams(queryString); 14 | try { 15 | if (urlParams.get('force')) { 16 | window.history.replaceState({}, document.title, "/"); 17 | throw new Error('Forced new identity') 18 | } 19 | var storedIdent = localStorage.getItem("identity") 20 | if (storedIdent === null) { 21 | throw new Error('No identity') 22 | } 23 | const restored = PrivateKey.fromString(storedIdent) 24 | return restored 25 | } 26 | catch (e) { 27 | /** 28 | * If any error, create a new identity. 29 | */ 30 | try { 31 | const identity = PrivateKey.fromRandom() 32 | const identityString = identity.toString() 33 | localStorage.setItem("identity", identityString) 34 | return identity 35 | } catch (err) { 36 | return err.message 37 | } 38 | } 39 | }); 40 | 41 | 42 | /** 43 | * More secure method for getting token & API auth. 44 | * 45 | * Keeps private key locally in the app. 46 | */ 47 | const loginWithChallenge = (identity: Identity): () => Promise => { 48 | // we pass identity into the function returning function to make it 49 | // available later in the callback 50 | return () => { 51 | return new Promise((resolve, reject) => { 52 | /** 53 | * Configured for our development server 54 | * 55 | * Note: this should be upgraded to wss for production environments. 56 | */ 57 | const socketUrl = `ws://localhost:3001/ws/userauth` 58 | 59 | /** Initialize our websocket connection */ 60 | const socket = new WebSocket(socketUrl) 61 | 62 | /** Wait for our socket to open successfully */ 63 | socket.onopen = () => { 64 | /** Get public key string */ 65 | const publicKey = identity.public.toString(); 66 | 67 | /** Send a new token request */ 68 | socket.send(JSON.stringify({ 69 | pubkey: publicKey, 70 | type: 'token' 71 | })); 72 | 73 | /** Listen for messages from the server */ 74 | socket.onmessage = async (event) => { 75 | const data = JSON.parse(event.data) 76 | switch (data.type) { 77 | /** Error never happen :) */ 78 | case 'error': { 79 | reject(data.value); 80 | break; 81 | } 82 | /** The server issued a new challenge */ 83 | case 'challenge':{ 84 | /** Convert the challenge json to a Buffer */ 85 | const buf = Buffer.from(data.value) 86 | /** User our identity to sign the challenge */ 87 | const signed = await identity.sign(buf) 88 | /** Send the signed challenge back to the server */ 89 | socket.send(JSON.stringify({ 90 | type: 'challenge', 91 | sig: Buffer.from(signed).toJSON() 92 | })); 93 | break; 94 | } 95 | /** New token generated */ 96 | case 'token': { 97 | resolve(data.value) 98 | break; 99 | } 100 | } 101 | } 102 | } 103 | }); 104 | } 105 | } 106 | 107 | /** 108 | * Method for using the server to create credentials without identity 109 | */ 110 | const createCredentials = async (): Promise => { 111 | const response = await fetch(`/api/userauth`, { 112 | method: 'GET', 113 | }) 114 | const userAuth: UserAuth = await response.json() 115 | return userAuth; 116 | } 117 | 118 | class HubClient { 119 | 120 | /** The users unique pki identity */ 121 | id?: PrivateKey 122 | 123 | /** The Hub API authentication */ 124 | client?: Client 125 | 126 | constructor () {} 127 | 128 | sign = async (buf: Buffer) => { 129 | if (!this.id) { 130 | throw Error('No user ID found') 131 | } 132 | return this.id.sign(buf) 133 | } 134 | setupIdentity = async () => { 135 | /** Create or get identity */ 136 | this.id = await getIdentity(); 137 | /** Contains the full identity (including private key) */ 138 | const identity = this.id.public.toString(); 139 | 140 | /** Render our avatar */ 141 | displayAvatar(identity) 142 | 143 | /** Get the public key */ 144 | const publicKey = this.id.public.toString(); 145 | 146 | /** Display the publicKey short ID */ 147 | displayIdentity(publicKey) 148 | } 149 | 150 | listThreads = async () => { 151 | if (!this.client) { 152 | throw Error('User not authenticated') 153 | } 154 | /** Query for all the user's existing threads (expected none) */ 155 | const result = await this.client.listThreads() 156 | 157 | /** Display the results */ 158 | displayThreadsList(JSON.stringify(result.listList)); 159 | } 160 | 161 | /** 162 | * Provides a full login where 163 | * - pubkey is shared with the server 164 | * - identity challenge is fulfilled here, on client 165 | * - hub api token is sent from the server 166 | * 167 | * see index.html for example running this method 168 | */ 169 | login = async () => { 170 | if (!this.id) { 171 | throw Error('No user ID found') 172 | } 173 | 174 | /** Use the identity to request a new API token when needed */ 175 | const loginCallback = loginWithChallenge(this.id); 176 | this.client = Client.withUserAuth(loginCallback) 177 | 178 | console.log('Verified on Textile API') 179 | displayStatus(); 180 | } 181 | 182 | /** 183 | * Provides a basic auth where 184 | * - the server doesn't care about the user identity 185 | * - the server just provides user auth on any request 186 | * 187 | * see simple.html for example running this method 188 | */ 189 | simpleAuth = async () => { 190 | if (!this.id) { 191 | throw Error('No user ID found') 192 | } 193 | /** Use the simple auth REST endpoint to get API access */ 194 | /** The simple auth endpoint generates a user's Hub API Token */ 195 | const client = Client.withUserAuth(createCredentials) 196 | /** getToken will get and store the user token in the Client */ 197 | await client.getToken(this.id) 198 | 199 | /** Update our auth to include the token */ 200 | this.client = client 201 | 202 | console.log('Verified on Textile API') 203 | displayStatus(); 204 | } 205 | } 206 | 207 | (window).HubClient = HubClient; -------------------------------------------------------------------------------- /hub-browser-auth-app/src/client/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Textile Hub - Token Demo 6 | 7 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 | NO TOKEN 28 |
29 |
30 |
31 | THREADS 32 |
33 | 34 |
35 |
36 |
37 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /hub-browser-auth-app/src/client/static/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: #fff; 3 | -webkit-font-smoothing: antialiased !important; 4 | } 5 | 6 | body, 7 | input, 8 | button, 9 | textarea{ 10 | font-family: "Arial", sans-serif; 11 | } 12 | 13 | .content { 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | flex-direction: column; 18 | width: 100%; 19 | text-align: center; 20 | margin: 10% 0; 21 | min-height: 400px; 22 | align-items: center; 23 | color: #514E4C; 24 | } 25 | .status { 26 | width: 25%; 27 | min-width: 200px; 28 | max-width: 400px; 29 | margin: 30px auto 0 auto; 30 | border-radius: 4px; 31 | border: 1px solid #E8E8E8; 32 | background: #fff; 33 | display: flex; 34 | flex-direction: column; 35 | } 36 | 37 | .status .id { 38 | padding-top: 15px; 39 | margin: 0 auto; 40 | width: 100%; 41 | justify-content: center; 42 | align-items: center; 43 | background: #F7F7F7; 44 | color: #2B2825; 45 | } 46 | 47 | .status .id .avatar { 48 | margin: 0 auto; 49 | border-radius: 50%; 50 | overflow: hidden; 51 | width: 80px; 52 | height: 80px; 53 | } 54 | .status .id .avatar svg { 55 | width: 100%; 56 | height: 100%; 57 | object-fit: cover; 58 | } 59 | 60 | .status .id .identity { 61 | margin: 15px auto 15px auto; 62 | text-align: center; 63 | font-weight: 500; 64 | font-size: 16px; 65 | } 66 | .status .id .identity strong { 67 | font-weight: 100; 68 | } 69 | .status .id .identity span { 70 | letter-spacing: -1px; 71 | } 72 | 73 | .status .token, .status .token { 74 | min-height: 150px; 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | flex-direction: column; 79 | } 80 | .status .token, .status { 81 | font-size: 18px; 82 | text-align: center; 83 | } 84 | .status .token { 85 | color: #aaa; 86 | } 87 | .status .token.verified { 88 | color: #514E4C; 89 | -webkit-transition: all 2s; 90 | -moz-transition: all 2s; 91 | -ms-transition: all 2s; 92 | -o-transition: all 2s; 93 | transition: all 2s; 94 | } 95 | 96 | .status .threads .threads-header { 97 | padding-bottom: 0.5rem; 98 | } 99 | .status .threads textarea { 100 | padding: 0.5rem; 101 | height: 100px; 102 | resize: none; 103 | background: transparent; 104 | border: 0; 105 | color: #aaa; 106 | font-size: 12px; 107 | text-align: center; 108 | } 109 | 110 | .corner-ribbon { 111 | width: 200px; 112 | background: #F7F7F7; 113 | border: 1px solid #ececec; 114 | position: absolute; 115 | top: 25px; 116 | left: -50px; 117 | text-align: center; 118 | line-height: 50px; 119 | letter-spacing: 1px; 120 | font-size: 12px; 121 | color: white; 122 | transform: rotate(-45deg); 123 | -webkit-transform: rotate(-45deg); 124 | } 125 | .corner-ribbon.top-right{ 126 | top: 25px; 127 | right: -50px; 128 | left: auto; 129 | transform: rotate(45deg); 130 | -webkit-transform: rotate(45deg); 131 | } 132 | .corner-ribbon a { 133 | text-decoration: none; 134 | color: gray; 135 | }; -------------------------------------------------------------------------------- /hub-browser-auth-app/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "downlevelIteration": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "react", 21 | "outDir": "../../dist/client", 22 | "rootDir": "./", 23 | "baseUrl": "../.." 24 | }, 25 | "exclude": [ 26 | "node_modules" 27 | ] 28 | } -------------------------------------------------------------------------------- /hub-browser-auth-app/src/client/ui.ts: -------------------------------------------------------------------------------- 1 | import { template } from 'lodash'; 2 | import jdenticon from 'jdenticon'; 3 | 4 | /** 5 | * Display the identity short string 6 | */ 7 | export const displayIdentity = (publicKey: string) => { 8 | const shortId = publicKey.substr(publicKey.length - 11, 10) 9 | const outputElement = document.getElementById('publicKey'); 10 | if (outputElement) { 11 | var compiled = template(` 12 | identity: <%- shortId %> 13 | `.trim()); 14 | outputElement.innerHTML = compiled({ 15 | shortId, 16 | }); 17 | } 18 | } 19 | 20 | /** 21 | * Display an avatar based on the user identity 22 | */ 23 | export const displayAvatar = (identity: string) => { 24 | const outputElement = document.getElementById('identity'); 25 | if (outputElement) { 26 | var compiled = template(` 27 | 28 | `.trim()); 29 | outputElement.innerHTML = compiled({ 30 | identity, 31 | }); 32 | /** trigger rendering */ 33 | jdenticon(); 34 | } 35 | } 36 | /** 37 | * Notify the user that they are verified on the API 38 | */ 39 | export const displayStatus = () => { 40 | const tokenElement = document.getElementById('token'); 41 | if (tokenElement) { 42 | tokenElement.classList.add("verified"); 43 | } 44 | /** Timeout just so the UI changes some after it loads :) */ 45 | setTimeout(()=> { 46 | const labelElement = document.getElementById('token-label'); 47 | if (labelElement) { 48 | labelElement.innerHTML = 'API AVAILABLE' 49 | } 50 | }, 800) 51 | } 52 | 53 | /** 54 | * Display any user threads created 55 | */ 56 | export const displayThreadsList = (threads: string) => { 57 | /** Timeout just so the UI changes some after it loads :) */ 58 | setTimeout(() => { 59 | const threadsElement = document.getElementById('threads-list'); 60 | if (threadsElement) { 61 | threadsElement.classList.add("verified"); 62 | threadsElement.innerHTML = threads; 63 | } 64 | }, 1000) 65 | } 66 | -------------------------------------------------------------------------------- /hub-browser-auth-app/src/server/api.ts: -------------------------------------------------------------------------------- 1 | /** Import our server libraries */ 2 | import koa from "koa"; 3 | import Router from "koa-router"; 4 | import { UserAuth } from "@textile/hub" 5 | 6 | import { getAPISig } from './hub-helpers'; 7 | 8 | 9 | /** 10 | * Start API Routes 11 | * 12 | * All prefixed with `/api/` 13 | */ 14 | const api = new Router({ 15 | prefix: '/api' 16 | }); 17 | 18 | /** 19 | * Create a REST API endpoint at /api/auth 20 | * 21 | * This endpoint will provide authorization for _any_ user. 22 | */ 23 | api.get( '/userauth', async (ctx: koa.Context, next: () => Promise) => { 24 | /** Get API authorization for the user */ 25 | const auth = await getAPISig() 26 | 27 | /** Include the token in the auth payload */ 28 | const credentials: UserAuth = { 29 | ...auth, 30 | key: process.env.USER_API_KEY, 31 | }; 32 | 33 | /** Return the auth in a JSON object */ 34 | ctx.body = credentials 35 | 36 | await next(); 37 | }); 38 | 39 | export default api; -------------------------------------------------------------------------------- /hub-browser-auth-app/src/server/hub-helpers.ts: -------------------------------------------------------------------------------- 1 | import { createAPISig, Client } from '@textile/hub'; 2 | 3 | /** 4 | * getAPISig uses helper function to create a new sig 5 | * 6 | * seconds (300) time until the sig expires 7 | */ 8 | export const getAPISig = async (seconds: number = 300) => { 9 | const expiration = new Date(Date.now() + 1000 * seconds) 10 | return await createAPISig(process.env.USER_API_SECRET, expiration) 11 | } 12 | 13 | /** 14 | * newClientDB creates a Client (remote DB) connection to the Hub 15 | * 16 | * A Hub connection is required to use the getToken API 17 | */ 18 | export const newClientDB = async () => { 19 | const API = process.env.API || undefined 20 | const db = await Client.withKeyInfo({ 21 | key: process.env.USER_API_KEY, 22 | secret: process.env.USER_API_SECRET 23 | }, API) 24 | return db; 25 | } -------------------------------------------------------------------------------- /hub-browser-auth-app/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import koa from "koa"; 2 | import Router from "koa-router"; 3 | import logger from "koa-logger"; 4 | import json from "koa-json"; 5 | import bodyParser from "koa-bodyparser"; 6 | import serve from "koa-static"; 7 | import websockify from "koa-websocket"; 8 | import cors from "@koa/cors"; 9 | 10 | import { createReadStream } from 'fs'; 11 | import dotenv from "dotenv"; 12 | 13 | import wss from "./wss"; 14 | import api from "./api"; 15 | 16 | dotenv.config(); 17 | 18 | if ( 19 | !process.env.USER_API_KEY || 20 | !process.env.USER_API_SECRET 21 | ) { 22 | process.exit(1); 23 | } 24 | 25 | const PORT = parseInt(process.env.PORT, 10) || 3001; 26 | 27 | const app = websockify(new koa()); 28 | 29 | /** Middlewares */ 30 | app.use( json() ); 31 | app.use( logger() ); 32 | app.use( bodyParser() ); 33 | 34 | /* Not safe in production */ 35 | app.use(cors()); 36 | 37 | app.use(serve(__dirname + '/../client')); 38 | 39 | /** 40 | * Start HTTP Routes 41 | */ 42 | const router = new Router(); 43 | app.use( router.routes() ).use( router.allowedMethods() ); 44 | 45 | /** 46 | * Serve index.html 47 | */ 48 | router.get( '/', async (ctx: koa.Context, next: () => Promise) => { 49 | ctx.type = 'text/html; charset=utf-8'; 50 | ctx.body = createReadStream(__dirname + '/../client/index.html'); 51 | await next(); 52 | }); 53 | 54 | /** 55 | * Create Rest endpoint for server-side token issue 56 | * 57 | * See ./api.ts 58 | */ 59 | app.use( api.routes() ); 60 | app.use( api.allowedMethods() ); 61 | 62 | /** 63 | * Create Websocket endpoint for client-side token challenge 64 | * 65 | * See ./wss.ts 66 | */ 67 | app.ws.use(wss); 68 | 69 | /** Start the server! */ 70 | app.listen( PORT, () => console.log( "Server started." ) ); -------------------------------------------------------------------------------- /hub-browser-auth-app/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "../../dist/server", 9 | "rootDir": "./", 10 | "baseUrl": "../.." 11 | }, 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /hub-browser-auth-app/src/server/wss.ts: -------------------------------------------------------------------------------- 1 | import route from "koa-route"; 2 | import Emittery from "emittery"; 3 | import { UserAuth } from "@textile/hub" 4 | 5 | import { newClientDB, getAPISig } from "./hub-helpers" 6 | 7 | interface UserModel { 8 | pubkey: string 9 | lastSeen: Date 10 | } 11 | 12 | /** 13 | * In a real system you might have a real user-singup flow 14 | * Here, we just stub in a basic user "database". 15 | * Users are added by their Public Key. 16 | * Users will only be added if they prove they hold the private key. 17 | * Proof is done using the Hub's built in token challenge API. 18 | */ 19 | const UserDB: {[key: string]: UserModel} = {} 20 | 21 | /** 22 | * This login includes a more thorough identity verification step. 23 | * 24 | * It leverages the Hub's public key verification via challenge. 25 | * The challenge is issued server-side by fulfilled here, client-side. 26 | * This has several benefits. 27 | * - User private key never needs to leave the user/client. 28 | * - The server will leverage the Hub verification in the process of user registration. 29 | * - The server can maintain a record of: user public key and user token in list of users. 30 | */ 31 | const wss = route.all('/ws/userauth', (ctx) => { 32 | /** Emittery allows us to wait for the challenge response event */ 33 | const emitter = new Emittery(); 34 | ctx.websocket.on('message', async (msg) => { 35 | try { 36 | /** All messages from client contain {type: string} */ 37 | const data = JSON.parse(msg); 38 | switch (data.type) { 39 | /** The first type is a new token request */ 40 | case 'token': { 41 | /** A new token request will contain the user's public key */ 42 | if (!data.pubkey) { throw new Error('missing pubkey') } 43 | 44 | /** 45 | * Init new Hub API Client 46 | * 47 | * see ./hub.ts 48 | */ 49 | const db = await newClientDB() 50 | /** Request a token from the Hub based on the user public key */ 51 | const token = await db.getTokenChallenge( 52 | data.pubkey, 53 | /** The callback passes the challenge back to the client */ 54 | (challenge: Uint8Array) => { 55 | return new Promise((resolve, reject) => { 56 | /** Pass the challenge to the client */ 57 | ctx.websocket.send(JSON.stringify({ 58 | type: 'challenge', 59 | value: Buffer.from(challenge).toJSON(), 60 | })) 61 | /** Wait for the challenge event from our event emitter */ 62 | emitter.on('challenge', (sig) => { 63 | /** Resolve the promise with the challenge response */ 64 | resolve(Buffer.from(sig)) 65 | }); 66 | /** Give client a reasonable timeout to respond to the challenge */ 67 | setTimeout(() => { 68 | reject() 69 | }, 1500); 70 | 71 | }) 72 | }) 73 | 74 | /** 75 | * The challenge was successfully completed by the client 76 | */ 77 | 78 | /** 79 | * The user has verified they own the pubkey. 80 | * Add or update the user in the user database 81 | */ 82 | const user: UserModel = { 83 | pubkey: data.pub, 84 | lastSeen: new Date(), 85 | } 86 | UserDB[data.pub] = user; 87 | 88 | /** Get API authorization for the user */ 89 | const auth = await getAPISig() 90 | 91 | /** Include the token in the auth payload */ 92 | const payload: UserAuth = { 93 | ...auth, 94 | token: token, 95 | key: process.env.USER_API_KEY, 96 | }; 97 | 98 | /** Return the result to the client */ 99 | ctx.websocket.send(JSON.stringify({ 100 | type: 'token', 101 | value: payload, 102 | })) 103 | break; 104 | } 105 | /** The second type is a challenge response */ 106 | case 'challenge': { 107 | /** A new challenge response will contain a signature */ 108 | if (!data.sig) { throw new Error('missing signature (sig)') } 109 | 110 | /** 111 | * If the timeout hasn't passed there is a waiting promise. 112 | * Emit the challenge signature for the waiting listener above. 113 | * */ 114 | await emitter.emit('challenge', data.sig); 115 | break; 116 | } 117 | } 118 | } catch (error) { 119 | /** Notify our client of any errors */ 120 | ctx.websocket.send(JSON.stringify({ 121 | type: 'error', 122 | value: error.message, 123 | })) 124 | } 125 | }); 126 | }); 127 | 128 | export default wss; 129 | -------------------------------------------------------------------------------- /hub-browser-auth-app/webpack.client.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: "development", 7 | devtool: "inline-source-map", 8 | entry: { 9 | main: "./src/client/index.ts", 10 | }, 11 | output: { 12 | path: path.resolve(__dirname, 'dist', 'client'), 13 | filename: "[name]-bundle.js", 14 | }, 15 | plugins: [ 16 | new CopyPlugin({ 17 | patterns: [ 18 | { 19 | from: '*.html', 20 | context: path.resolve(__dirname, 'src', 'client') 21 | }, 22 | { 23 | from: path.resolve(__dirname, 'src/client/static'), 24 | to: path.resolve(__dirname, 'dist/client/static'), 25 | }, 26 | ], 27 | }), 28 | ], 29 | resolve: { 30 | plugins: [ 31 | new TsconfigPathsPlugin({ configFile: "./src/client/tsconfig.json" }) 32 | ], 33 | // Add ".ts" and ".tsx" as resolvable extensions. 34 | extensions: [".ts", ".tsx", ".js"], 35 | }, 36 | module: { 37 | rules: [ 38 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 39 | { 40 | test: /\.tsx?$/, 41 | loader: "ts-loader" 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /metamask-identities-ed25519/README.md: -------------------------------------------------------------------------------- 1 | See full [README.md here](../README.md). 2 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metamask-textile-identities", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "11.0.0-next.12", 7 | "@textile/hub": "^6.0.1", 8 | "@types/bcryptjs": "^2.4.2", 9 | "bcryptjs": "^2.4.3", 10 | "ethers": "^5.0.8", 11 | "react": "^16.13.1", 12 | "react-dom": "^16.13.1", 13 | "react-fontawesome": "^1.7.1", 14 | "react-scripts": "3.4.1", 15 | "slate-react-system": "^0.1.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^14.0.14", 19 | "@types/react": "^16.9.41", 20 | "@types/react-dom": "^16.9.8", 21 | "@types/react-fontawesome": "^1.6.4", 22 | "http-proxy-middleware": "^1.0.4", 23 | "typescript": "^3.9.5" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "eject": "react-scripts eject", 29 | "textile:check": "npx ncu '/^@textile/.*$/'", 30 | "textile:upgrade": "npx ncu -u '/^@textile/.*$/'" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/metamask-identities-ed25519/public/favicon.ico -------------------------------------------------------------------------------- /metamask-identities-ed25519/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/metamask-identities-ed25519/public/logo192.png -------------------------------------------------------------------------------- /metamask-identities-ed25519/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/metamask-identities-ed25519/public/logo512.png -------------------------------------------------------------------------------- /metamask-identities-ed25519/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // @ts-ignore 3 | import { dispatchCustomEvent, ButtonPrimary, GlobalNotification, Input } from 'slate-react-system' 4 | import { PrivateKey } from '@textile/hub' 5 | import { BigNumber, providers, utils } from 'ethers' 6 | import { hashSync } from 'bcryptjs' 7 | 8 | /** 9 | * Metamask signing and seed generation adapted from pnlp project. See sources here, 10 | * https://github.com/pnlp-network/pnlp/blob/91540abea8b51231c2f1e2fe8cc03b7604842d03/pnlp-app/src/app/%40core/persistence/blockchain.service.ts 11 | * https://github.com/pnlp-network/pnlp/blob/91540abea8b51231c2f1e2fe8cc03b7604842d03/pnlp-app/src/app/%40core/persistence/keystore.service.ts 12 | */ 13 | 14 | type WindowInstanceWithEthereum = Window & typeof globalThis & { ethereum?: providers.ExternalProvider }; 15 | class StrongType { 16 | // @ts-ignore 17 | private _type: Definition; 18 | constructor(public value?: Type) {} 19 | } 20 | export class EthereumAddress extends StrongType<'ethereum_address', string> {} 21 | 22 | class App extends React.Component { 23 | state = { 24 | secret: '' 25 | } 26 | handleChange = (e: any) => this.setState({ [e.target.name]: e.target.value }); 27 | 28 | private generateMessageForEntropy(ethereum_address: EthereumAddress, application_name: string, secret: string): string { 29 | return ( 30 | '******************************************************************************** \n' + 31 | 'READ THIS MESSAGE CAREFULLY. \n' + 32 | 'DO NOT SHARE THIS SIGNED MESSAGE WITH ANYONE OR THEY WILL HAVE READ AND WRITE \n' + 33 | 'ACCESS TO THIS APPLICATION. \n' + 34 | 'DO NOT SIGN THIS MESSAGE IF THE FOLLOWING IS NOT TRUE OR YOU DO NOT CONSENT \n' + 35 | 'TO THE CURRENT APPLICATION HAVING ACCESS TO THE FOLLOWING APPLICATION. \n' + 36 | '******************************************************************************** \n' + 37 | 'The Ethereum address used by this application is: \n' + 38 | '\n' + 39 | ethereum_address.value + 40 | '\n' + 41 | '\n' + 42 | '\n' + 43 | 'By signing this message, you authorize the current application to use the \n' + 44 | 'following app associated with the above address: \n' + 45 | '\n' + 46 | application_name + 47 | '\n' + 48 | '\n' + 49 | '\n' + 50 | 'The hash of your non-recoverable, private, non-persisted password or secret \n' + 51 | 'phrase is: \n' + 52 | '\n' + 53 | secret + 54 | '\n' + 55 | '\n' + 56 | '\n' + 57 | '******************************************************************************** \n' + 58 | 'ONLY SIGN THIS MESSAGE IF YOU CONSENT TO THE CURRENT PAGE ACCESSING THE KEYS \n' + 59 | 'ASSOCIATED WITH THE ABOVE ADDRESS AND APPLICATION. \n' + 60 | 'AGAIN, DO NOT SHARE THIS SIGNED MESSAGE WITH ANYONE OR THEY WILL HAVE READ AND \n' + 61 | 'WRITE ACCESS TO THIS APPLICATION. \n' + 62 | '******************************************************************************** \n' 63 | ); 64 | } 65 | 66 | getSigner = async () => { 67 | if (!(window as WindowInstanceWithEthereum).ethereum) { 68 | throw new Error( 69 | 'Ethereum is not connected. Please download Metamask from https://metamask.io/download.html' 70 | ); 71 | } 72 | 73 | console.debug('Initializing web3 provider...'); 74 | // @ts-ignore 75 | const provider = new providers.Web3Provider((window as WindowInstanceWithEthereum).ethereum); 76 | const signer = provider.getSigner(); 77 | return signer 78 | } 79 | 80 | public async getAddressAndSigner(): Promise<{address: EthereumAddress, signer: any}> { 81 | const signer = await this.getSigner() 82 | // @ts-ignore 83 | const accounts = await (window as WindowInstanceWithEthereum).ethereum.request({ method: 'eth_requestAccounts' }); 84 | if (accounts.length === 0) { 85 | throw new Error('No account is provided. Please provide an account to this application.'); 86 | } 87 | 88 | const address = new EthereumAddress(accounts[0]); 89 | 90 | return {address, signer} 91 | } 92 | generatePrivateKey = async (): Promise => { 93 | const metamask = await this.getAddressAndSigner() 94 | // avoid sending the raw secret by hashing it first 95 | const secret = hashSync(this.state.secret, 10) 96 | const message = this.generateMessageForEntropy(metamask.address, 'textile-demo', secret) 97 | const signedText = await metamask.signer.signMessage(message); 98 | const hash = utils.keccak256(signedText); 99 | if (hash === null) { 100 | throw new Error('No account is provided. Please provide an account to this application.'); 101 | } 102 | // The following line converts the hash in hex to an array of 32 integers. 103 | // @ts-ignore 104 | const array = hash 105 | // @ts-ignore 106 | .replace('0x', '') 107 | // @ts-ignore 108 | .match(/.{2}/g) 109 | .map((hexNoPrefix) => BigNumber.from('0x' + hexNoPrefix).toNumber()) 110 | 111 | if (array.length !== 32) { 112 | throw new Error('Hash of signature is not the correct size! Something went wrong!'); 113 | } 114 | const identity = PrivateKey.fromRawEd25519Seed(Uint8Array.from(array)) 115 | console.log(identity.toString()) 116 | 117 | this.createNotification(identity) 118 | 119 | // Your app can now use this identity for generating a user Mailbox, Threads, Buckets, etc 120 | return identity 121 | } 122 | 123 | createNotification = (identity: PrivateKey) => { 124 | dispatchCustomEvent({ name: "create-notification", detail: { 125 | id: 1, 126 | description: `PubKey: ${identity.public.toString()}. Your app can now generate and reuse this users PrivateKey for creating user Mailboxes, Threads, and Buckets.`, 127 | timeout: 5000, 128 | }}); 129 | } 130 | 131 | render () { 132 | return ( 133 |
134 | 135 |
136 | 146 | Login with Metamask 147 |
148 |
149 | ) 150 | } 151 | } 152 | 153 | export default App; 154 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/src/index.css: -------------------------------------------------------------------------------- 1 | Html, body{ 2 | height:100%; 3 | } 4 | 5 | .container { 6 | display: flex; /* establish flex container */ 7 | flex-direction: column; /* make main axis vertical */ 8 | justify-content: center; /* center items vertically, in this case */ 9 | align-items: center; /* center items horizontally, in this case */ 10 | height: 300px; 11 | } 12 | 13 | .login { 14 | width: 300px; 15 | margin: 5px; 16 | text-align: center; 17 | } 18 | 19 | .login div, .login button, .login input { 20 | margin-top: 10px; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /metamask-identities-ed25519/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "downlevelIteration": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | 28 | -------------------------------------------------------------------------------- /npm-publish-bucket/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /npm-publish-bucket/README.md: -------------------------------------------------------------------------------- 1 | # Publish to a bucket from NPM 2 | 3 | ## Install 4 | 5 | ```sh 6 | npm install @textile/buck-util 7 | ``` 8 | 9 | ## Configure 10 | 11 | Next, add a `publish` script to your `package.json`. See the package.json in this folder for example. Determine where you `npm run build` step outputs, below assumes it outputs to a folder called, `build`. The publish script is of the form: `buck-util push {BUILD-FOLDER}`. 12 | 13 | ``` 14 | ... 15 | "publish": "buck-util push build", 16 | ... 17 | ``` 18 | 19 | ## Add secrets 20 | 21 | Create a `.env` file in the root of your project. This file should be included in `.gitignore` and not shared publically. You can see `env.example` as a guide. 22 | 23 | - API key and secret: generate a new Account Key with the Hub CLI 24 | - Thread: This is ID of the Thread where you will push your bucket. You can manually create your bucket the first time (`hub buck init`) and copy the thread id from there. It looks like, `bafkt2uayur6cxo6oyskpebtrbttahktppoeuvs43myytqzz3oer5bca`. 25 | - Bucket name: A static name for the bucket you are pushing. It should match the one you create with `hub buck init`. 26 | 27 | ## Publish 28 | 29 | Run `npm run publish` to push updates from your build folder. 30 | 31 | -------------------------------------------------------------------------------- /npm-publish-bucket/example.env: -------------------------------------------------------------------------------- 1 | HUB_API_KEY= 2 | HUB_API_SECRET= 3 | HUB_THREAD= 4 | HUB_BUCKET_NAME= -------------------------------------------------------------------------------- /npm-publish-bucket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-publish-bucket", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "5.11.9", 7 | "@testing-library/react": "11.2.5", 8 | "@testing-library/user-event": "12.6.3", 9 | "react": "^17.0.1", 10 | "react-dom": "^17.0.1", 11 | "react-scripts": "4.0.2", 12 | "web-vitals": "1.1.0" 13 | }, 14 | "scripts": { 15 | "publish": "buck-util push build", 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "@textile/buck-util": "1.0.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /npm-publish-bucket/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/npm-publish-bucket/public/favicon.ico -------------------------------------------------------------------------------- /npm-publish-bucket/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /npm-publish-bucket/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/npm-publish-bucket/public/logo192.png -------------------------------------------------------------------------------- /npm-publish-bucket/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/npm-publish-bucket/public/logo512.png -------------------------------------------------------------------------------- /npm-publish-bucket/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /npm-publish-bucket/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /npm-publish-bucket/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /npm-publish-bucket/src/App.js: -------------------------------------------------------------------------------- 1 | import logo from './logo.svg'; 2 | import './App.css'; 3 | 4 | function App() { 5 | return ( 6 |
7 |
8 | logo 9 |

10 | Edit src/App.js and save to reload. 11 |

12 | 18 | Learn React 19 | 20 |
21 |
22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /npm-publish-bucket/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /npm-publish-bucket/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /npm-publish-bucket/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /npm-publish-bucket/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /npm-publish-bucket/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /npm-publish-bucket/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /react-native-hub-app/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /react-native-hub-app/.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/docs 3 | **/dist 4 | karma.conf.js 5 | -------------------------------------------------------------------------------- /react-native-hub-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "commonjs": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "plugin:prettier/recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "prettier/@typescript-eslint", 12 | "plugin:import/typescript" 13 | ], 14 | "plugins": [ 15 | "@typescript-eslint", 16 | "prettier", 17 | "import" 18 | ], 19 | "globals": { 20 | "Atomics": "readonly", 21 | "SharedArrayBuffer": "readonly" 22 | }, 23 | "parser": "@typescript-eslint/parser", 24 | "parserOptions": { 25 | "ecmaVersion": 2018, 26 | "sourceType": "module" 27 | }, 28 | "rules": { 29 | "prettier/prettier": "error", 30 | "@typescript-eslint/explicit-function-return-type": 0, 31 | "@typescript-eslint/no-explicit-any": 0, 32 | "@typescript-eslint/explicit-member-accessibility": 0, 33 | "@typescript-eslint/no-unused-vars": [0, {"argsIgnorePattern": "^_"}], 34 | "no-undefined": 0, 35 | "@typescript-eslint/ban-ts-ignore": 0, 36 | "default-case": 0, 37 | "import/no-absolute-path": 2, 38 | "import/no-cycle": 1, 39 | "import/export": 2, 40 | "import/no-named-as-default": 2, 41 | "import/no-named-as-default-member": 2, 42 | "import/first": 2, 43 | "import/order": 2, 44 | "import/newline-after-import": 2, 45 | "import/no-default-export": 0 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /react-native-hub-app/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore polyfills 9 | node_modules/react-native/Libraries/polyfills/.* 10 | 11 | ; These should not be required directly 12 | ; require from fbjs/lib instead: require('fbjs/lib/warning') 13 | node_modules/warning/.* 14 | 15 | ; Flow doesn't support platforms 16 | .*/Libraries/Utilities/LoadingView.js 17 | 18 | [untyped] 19 | .*/node_modules/@react-native-community/cli/.*/.* 20 | 21 | [include] 22 | 23 | [libs] 24 | node_modules/react-native/interface.js 25 | node_modules/react-native/flow/ 26 | 27 | [options] 28 | emoji=true 29 | 30 | esproposal.optional_chaining=enable 31 | esproposal.nullish_coalescing=enable 32 | 33 | module.file_ext=.js 34 | module.file_ext=.json 35 | module.file_ext=.ios.js 36 | 37 | munge_underscores=true 38 | 39 | module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' 40 | module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' 41 | 42 | suppress_type=$FlowIssue 43 | suppress_type=$FlowFixMe 44 | suppress_type=$FlowFixMeProps 45 | suppress_type=$FlowFixMeState 46 | 47 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) 48 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ 49 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 50 | 51 | [lints] 52 | sketchy-null-number=warn 53 | sketchy-null-mixed=warn 54 | sketchy-number=warn 55 | untyped-type-import=warn 56 | nonstrict-import=warn 57 | deprecated-type=warn 58 | unsafe-getters-setters=warn 59 | inexact-spread=warn 60 | unnecessary-invariant=warn 61 | signature-verification-failure=warn 62 | deprecated-utility=error 63 | 64 | [strict] 65 | deprecated-type 66 | nonstrict-import 67 | sketchy-null 68 | unclear-type 69 | unsafe-getters-setters 70 | untyped-import 71 | untyped-type-import 72 | 73 | [version] 74 | ^0.113.0 75 | -------------------------------------------------------------------------------- /react-native-hub-app/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /react-native-hub-app/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | -------------------------------------------------------------------------------- /react-native-hub-app/.prettierignore: -------------------------------------------------------------------------------- 1 | **/docs/ 2 | **/dist/ 3 | **/node_modules/ 4 | **/*.d.ts 5 | -------------------------------------------------------------------------------- /react-native-hub-app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | printWidth: 100, 4 | trailingComma: "all", 5 | singleQuote: true, 6 | tabWidth: 2 7 | }; 8 | -------------------------------------------------------------------------------- /react-native-hub-app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /react-native-hub-app/App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | * @flow strict-local 4 | */ 5 | 6 | import React from 'react'; 7 | import {StyleSheet, View, StatusBar} from 'react-native'; 8 | import CheckList from './src/checklist'; 9 | 10 | const App: () => React$Node = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | const styles = StyleSheet.create({ 20 | container: { 21 | flex: 1, 22 | padding: 8, 23 | flexDirection: 'column', // main axis 24 | justifyContent: 'center', // main axis 25 | alignItems: 'center', // cross axis 26 | backgroundColor: '#FEFEFE', 27 | }, 28 | }); 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /react-native-hub-app/README.md: -------------------------------------------------------------------------------- 1 | ## Setup 2 | 3 | This project is already setup to run on mobile apps via React Native. All you need to do is `npm install`. 4 | 5 | To create your own React Native apps, you can use this example as a guide. In particular, be sure to use the `postinstall` script you'll see in the `package.json`, this postinstall step will create the `shim.js` file you need to run Threads in your React Native. Next, be sure to copy the `import './shim';` line you'll find in `index.js` to the top of your own `index.js`. 6 | 7 | ### Textile Hub Key & Secret 8 | 9 | ``` 10 | npm install --save react-native-dotenv 11 | mv example.env .env 12 | ``` 13 | 14 | edit `.env`. add your own api key and secret from `hub`, the [Textile Hub CLI](https://docs.textile.io/hub/accounts/). 15 | 16 | ## App 17 | 18 | The app is intentionally simple. All the logic is contained in a single view (`Tests.js`). The view state contains a series of tests, each is run one after the other. After each test runs, the row in the UI for that test is updated with success or failure indicator. 19 | 20 | [App Preview](https://github.com/textileio/js-examples/blob/master/react-native-client-app/preview.gif) 21 | 22 | ## Available Scripts 23 | 24 | In the project directory, you can run: 25 | 26 | ### `npm run android` 27 | 28 | Runs the app in the development mode and on the Android emulator. The app will reload if you make edits. 29 | 30 | The app should display a series of Tests directly in the UI, those tests should all succeed if you've set the app up correctly. 31 | 32 | ## Run 33 | 34 | ``` 35 | npm install 36 | npm run android 37 | ``` 38 | 39 | ### Critical 40 | 41 | Following each install, the `postinstall` step will run to ensure the node methods are added to the global context. 42 | 43 | ``` 44 | "postinstall": "./node_modules/.bin/rn-nodeify --install fs,path,process,buffer,crypto,stream,vm --hack" 45 | ``` 46 | -------------------------------------------------------------------------------- /react-native-hub-app/__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /react-native-hub-app/android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.threadsdb_app", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.threadsdb_app", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /react-native-hub-app/android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /react-native-hub-app/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/app/debug.keystore -------------------------------------------------------------------------------- /react-native-hub-app/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/debug/java/com/threadsdb_app/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.threadsdb_app; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 22 | import com.facebook.react.ReactInstanceManager; 23 | import com.facebook.react.bridge.ReactContext; 24 | import com.facebook.react.modules.network.NetworkingModule; 25 | import okhttp3.OkHttpClient; 26 | 27 | public class ReactNativeFlipper { 28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 29 | if (FlipperUtils.shouldEnableFlipper(context)) { 30 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 31 | 32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 33 | client.addPlugin(new ReactFlipperPlugin()); 34 | client.addPlugin(new DatabasesFlipperPlugin(context)); 35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 36 | client.addPlugin(CrashReporterPlugin.getInstance()); 37 | 38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 39 | NetworkingModule.setCustomClientBuilder( 40 | new NetworkingModule.CustomClientBuilder() { 41 | @Override 42 | public void apply(OkHttpClient.Builder builder) { 43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 44 | } 45 | }); 46 | client.addPlugin(networkFlipperPlugin); 47 | client.start(); 48 | 49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 50 | // Hence we run if after all native modules have been initialized 51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 52 | if (reactContext == null) { 53 | reactInstanceManager.addReactInstanceEventListener( 54 | new ReactInstanceManager.ReactInstanceEventListener() { 55 | @Override 56 | public void onReactContextInitialized(ReactContext reactContext) { 57 | reactInstanceManager.removeReactInstanceEventListener(this); 58 | reactContext.runOnNativeModulesQueueThread( 59 | new Runnable() { 60 | @Override 61 | public void run() { 62 | client.addPlugin(new FrescoFlipperPlugin()); 63 | } 64 | }); 65 | } 66 | }); 67 | } else { 68 | client.addPlugin(new FrescoFlipperPlugin()); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/java/com/threadsdb_app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.threadsdb_app; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. This is used to schedule 9 | * rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "threadsdb_app"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/java/com/threadsdb_app/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.threadsdb_app; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactInstanceManager; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.soloader.SoLoader; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.util.List; 13 | 14 | public class MainApplication extends Application implements ReactApplication { 15 | 16 | private final ReactNativeHost mReactNativeHost = 17 | new ReactNativeHost(this) { 18 | @Override 19 | public boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | @SuppressWarnings("UnnecessaryLocalVariable") 26 | List packages = new PackageList(this).getPackages(); 27 | // Packages that cannot be autolinked yet can be added manually here, for example: 28 | // packages.add(new MyReactNativePackage()); 29 | return packages; 30 | } 31 | 32 | @Override 33 | protected String getJSMainModuleName() { 34 | return "index"; 35 | } 36 | }; 37 | 38 | @Override 39 | public ReactNativeHost getReactNativeHost() { 40 | return mReactNativeHost; 41 | } 42 | 43 | @Override 44 | public void onCreate() { 45 | super.onCreate(); 46 | SoLoader.init(this, /* native exopackage */ false); 47 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 48 | } 49 | 50 | /** 51 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 52 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 53 | * 54 | * @param context 55 | * @param reactInstanceManager 56 | */ 57 | private static void initializeFlipper( 58 | Context context, ReactInstanceManager reactInstanceManager) { 59 | if (BuildConfig.DEBUG) { 60 | try { 61 | /* 62 | We use reflection here to pick up the class that initializes Flipper, 63 | since Flipper library is not available in release mode 64 | */ 65 | Class aClass = Class.forName("com.threadsdb_app.ReactNativeFlipper"); 66 | aClass 67 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 68 | .invoke(null, context, reactInstanceManager); 69 | } catch (ClassNotFoundException e) { 70 | e.printStackTrace(); 71 | } catch (NoSuchMethodException e) { 72 | e.printStackTrace(); 73 | } catch (IllegalAccessException e) { 74 | e.printStackTrace(); 75 | } catch (InvocationTargetException e) { 76 | e.printStackTrace(); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | threadsdb_app 3 | 4 | -------------------------------------------------------------------------------- /react-native-hub-app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /react-native-hub-app/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "28.0.3" 6 | minSdkVersion = 16 7 | compileSdkVersion = 28 8 | targetSdkVersion = 28 9 | } 10 | repositories { 11 | google() 12 | jcenter() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle:3.5.2") 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | mavenLocal() 25 | maven { 26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 27 | url("$rootDir/../node_modules/react-native/android") 28 | } 29 | maven { 30 | // Android JSC is installed from npm 31 | url("$rootDir/../node_modules/jsc-android/dist") 32 | } 33 | 34 | google() 35 | jcenter() 36 | maven { url 'https://www.jitpack.io' } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /react-native-hub-app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.33.1 29 | -------------------------------------------------------------------------------- /react-native-hub-app/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/react-native-hub-app/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /react-native-hub-app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /react-native-hub-app/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /react-native-hub-app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'threadsdb_app' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | -------------------------------------------------------------------------------- /react-native-hub-app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threadsdb_app", 3 | "displayName": "threads_app" 4 | } -------------------------------------------------------------------------------- /react-native-hub-app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | 'module:metro-react-native-babel-preset', 4 | 'module:react-native-dotenv', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /react-native-hub-app/example.env: -------------------------------------------------------------------------------- 1 | USER_API_SECRET=textile-hub-user-secret 2 | USER_API_KEY=textile-hub-user-key 3 | -------------------------------------------------------------------------------- /react-native-hub-app/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import './shim'; 6 | import * as encoding from 'text-encoding'; 7 | import {AppRegistry} from 'react-native'; 8 | import App from './App'; 9 | import {name as appName} from './app.json'; 10 | 11 | AppRegistry.registerComponent(appName, () => App); 12 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | def add_flipper_pods!(versions = {}) 5 | versions['Flipper'] ||= '~> 0.33.1' 6 | versions['DoubleConversion'] ||= '1.1.7' 7 | versions['Flipper-Folly'] ||= '~> 2.1' 8 | versions['Flipper-Glog'] ||= '0.3.6' 9 | versions['Flipper-PeerTalk'] ||= '~> 0.0.4' 10 | versions['Flipper-RSocket'] ||= '~> 1.0' 11 | 12 | pod 'FlipperKit', versions['Flipper'], :configuration => 'Debug' 13 | pod 'FlipperKit/FlipperKitLayoutPlugin', versions['Flipper'], :configuration => 'Debug' 14 | pod 'FlipperKit/SKIOSNetworkPlugin', versions['Flipper'], :configuration => 'Debug' 15 | pod 'FlipperKit/FlipperKitUserDefaultsPlugin', versions['Flipper'], :configuration => 'Debug' 16 | pod 'FlipperKit/FlipperKitReactPlugin', versions['Flipper'], :configuration => 'Debug' 17 | 18 | # List all transitive dependencies for FlipperKit pods 19 | # to avoid them being linked in Release builds 20 | pod 'Flipper', versions['Flipper'], :configuration => 'Debug' 21 | pod 'Flipper-DoubleConversion', versions['DoubleConversion'], :configuration => 'Debug' 22 | pod 'Flipper-Folly', versions['Flipper-Folly'], :configuration => 'Debug' 23 | pod 'Flipper-Glog', versions['Flipper-Glog'], :configuration => 'Debug' 24 | pod 'Flipper-PeerTalk', versions['Flipper-PeerTalk'], :configuration => 'Debug' 25 | pod 'Flipper-RSocket', versions['Flipper-RSocket'], :configuration => 'Debug' 26 | pod 'FlipperKit/Core', versions['Flipper'], :configuration => 'Debug' 27 | pod 'FlipperKit/CppBridge', versions['Flipper'], :configuration => 'Debug' 28 | pod 'FlipperKit/FBCxxFollyDynamicConvert', versions['Flipper'], :configuration => 'Debug' 29 | pod 'FlipperKit/FBDefines', versions['Flipper'], :configuration => 'Debug' 30 | pod 'FlipperKit/FKPortForwarding', versions['Flipper'], :configuration => 'Debug' 31 | pod 'FlipperKit/FlipperKitHighlightOverlay', versions['Flipper'], :configuration => 'Debug' 32 | pod 'FlipperKit/FlipperKitLayoutTextSearchable', versions['Flipper'], :configuration => 'Debug' 33 | pod 'FlipperKit/FlipperKitNetworkPlugin', versions['Flipper'], :configuration => 'Debug' 34 | end 35 | 36 | # Post Install processing for Flipper 37 | def flipper_post_install(installer) 38 | installer.pods_project.targets.each do |target| 39 | if target.name == 'YogaKit' 40 | target.build_configurations.each do |config| 41 | config.build_settings['SWIFT_VERSION'] = '4.1' 42 | end 43 | end 44 | end 45 | end 46 | 47 | target 'threadsdb_app' do 48 | # Pods for threadsdb_app 49 | pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector" 50 | pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec" 51 | pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired" 52 | pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety" 53 | pod 'React', :path => '../node_modules/react-native/' 54 | pod 'React-Core', :path => '../node_modules/react-native/' 55 | pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules' 56 | pod 'React-Core/DevSupport', :path => '../node_modules/react-native/' 57 | pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS' 58 | pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation' 59 | pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob' 60 | pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image' 61 | pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS' 62 | pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network' 63 | pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings' 64 | pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text' 65 | pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration' 66 | pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/' 67 | 68 | pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact' 69 | pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi' 70 | pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor' 71 | pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector' 72 | pod 'ReactCommon/callinvoker', :path => "../node_modules/react-native/ReactCommon" 73 | pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon" 74 | pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga', :modular_headers => true 75 | 76 | pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec' 77 | pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec' 78 | pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec' 79 | 80 | target 'threadsdb_appTests' do 81 | inherit! :complete 82 | # Pods for testing 83 | end 84 | 85 | use_native_modules! 86 | 87 | # Enables Flipper. 88 | # 89 | # Note that if you have use_frameworks! enabled, Flipper will not work and 90 | # you should disable these next few lines. 91 | add_flipper_pods! 92 | post_install do |installer| 93 | flipper_post_install(installer) 94 | end 95 | end 96 | 97 | target 'threadsdb_app-tvOS' do 98 | # Pods for threadsdb_app-tvOS 99 | 100 | target 'threadsdb_app-tvOSTests' do 101 | inherit! :search_paths 102 | # Pods for testing 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 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 | NSAppTransportSecurity 26 | 27 | NSExceptionDomains 28 | 29 | localhost 30 | 31 | NSExceptionAllowsInsecureHTTPLoads 32 | 33 | 34 | 35 | 36 | NSLocationWhenInUseUsageDescription 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(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 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app.xcodeproj/xcshareddata/xcschemes/threadsdb_app-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app.xcodeproj/xcshareddata/xcschemes/threadsdb_app.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | #if DEBUG 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | static void InitializeFlipper(UIApplication *application) { 16 | FlipperClient *client = [FlipperClient sharedClient]; 17 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; 18 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; 19 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; 20 | [client addPlugin:[FlipperKitReactPlugin new]]; 21 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; 22 | [client start]; 23 | } 24 | #endif 25 | 26 | @implementation AppDelegate 27 | 28 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 29 | { 30 | #if DEBUG 31 | InitializeFlipper(application); 32 | #endif 33 | 34 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 35 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 36 | moduleName:@"threadsdb_app" 37 | initialProperties:nil]; 38 | 39 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 40 | 41 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 42 | UIViewController *rootViewController = [UIViewController new]; 43 | rootViewController.view = rootView; 44 | self.window.rootViewController = rootViewController; 45 | [self.window makeKeyAndVisible]; 46 | return YES; 47 | } 48 | 49 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 50 | { 51 | #if DEBUG 52 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 53 | #else 54 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 55 | #endif 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app/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 | } -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | threadsdb_app 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSExceptionDomains 32 | 33 | localhost 34 | 35 | NSExceptionAllowsInsecureHTTPLoads 36 | 37 | 38 | 39 | 40 | NSLocationWhenInUseUsageDescription 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_app/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_appTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 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 | -------------------------------------------------------------------------------- /react-native-hub-app/ios/threadsdb_appTests/threadsdb_appTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface threadsdb_appTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation threadsdb_appTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 38 | if (level >= RCTLogLevelError) { 39 | redboxError = message; 40 | } 41 | }); 42 | #endif 43 | 44 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 45 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 46 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | 48 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 49 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 50 | return YES; 51 | } 52 | return NO; 53 | }]; 54 | } 55 | 56 | #ifdef DEBUG 57 | RCTSetLogFunction(RCTDefaultLogFunction); 58 | #endif 59 | 60 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 61 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 62 | } 63 | 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /react-native-hub-app/metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /react-native-hub-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threadsdb_app", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "textile:check": "npx ncu '/^@textile/.*$/'", 10 | "textile:upgrade": "npx ncu -u '/^@textile/.*$/'", 11 | "postinstall": "npx rn-nodeify --install fs,path,process,buffer,crypto,stream,vm --hack", 12 | "bind": "adb reverse tcp:3007 tcp:3007" 13 | }, 14 | "dependencies": { 15 | "@textile/hub": "^6.0.1", 16 | "bad-words": "^3.0.3", 17 | "buffer": "^4.9.2", 18 | "events": "^3.1.0", 19 | "path-browserify": "0.0.0", 20 | "process": "^0.11.10", 21 | "react": "16.11.0", 22 | "react-native": "0.62.2", 23 | "react-native-crypto": "^2.2.0", 24 | "react-native-dotenv": "^0.2.0", 25 | "react-native-input-prompt": "^1.0.0", 26 | "react-native-level-fs": "^3.0.1", 27 | "react-native-randombytes": "^3.5.3", 28 | "readable-stream": "^1.1.14", 29 | "rn-nodeify": "^10.2.0", 30 | "stream": "0.0.2", 31 | "stream-browserify": "^1.0.0", 32 | "text-encoding": "^0.7.0", 33 | "vm-browserify": "0.0.4" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.9.6", 37 | "@babel/runtime": "^7.9.6", 38 | "@react-native-community/eslint-config": "^1.1.0", 39 | "@types/react": "^16.9.35", 40 | "@types/react-native": "^0.62.8", 41 | "@typescript-eslint/eslint-plugin": "^2.33.0", 42 | "@typescript-eslint/parser": "^2.33.0", 43 | "babel-jest": "^26.0.1", 44 | "eslint": "^7.0.0", 45 | "eslint-import-resolver-typescript": "^2.0.0", 46 | "jest": "^26.0.1", 47 | "metro-react-native-babel-preset": "^0.59.0", 48 | "npm-check-updates": "^6.0.1", 49 | "react-test-renderer": "16.11.0", 50 | "typescript": "^3.9.2" 51 | }, 52 | "jest": { 53 | "preset": "react-native" 54 | }, 55 | "react-native": { 56 | "crypto": "react-native-crypto", 57 | "path": "path-browserify", 58 | "fs": "react-native-level-fs", 59 | "_stream_transform": "readable-stream/transform", 60 | "_stream_readable": "readable-stream/readable", 61 | "_stream_writable": "readable-stream/writable", 62 | "_stream_duplex": "readable-stream/duplex", 63 | "_stream_passthrough": "readable-stream/passthrough", 64 | "stream": "stream-browserify", 65 | "vm": "vm-browserify" 66 | }, 67 | "browser": { 68 | "crypto": "react-native-crypto", 69 | "path": "path-browserify", 70 | "fs": "react-native-level-fs", 71 | "_stream_transform": "readable-stream/transform", 72 | "_stream_readable": "readable-stream/readable", 73 | "_stream_writable": "readable-stream/writable", 74 | "_stream_duplex": "readable-stream/duplex", 75 | "_stream_passthrough": "readable-stream/passthrough", 76 | "stream": "stream-browserify", 77 | "vm": "vm-browserify" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /react-native-hub-app/shim.js: -------------------------------------------------------------------------------- 1 | if (typeof __dirname === 'undefined') global.__dirname = '/' 2 | if (typeof __filename === 'undefined') global.__filename = '' 3 | if (typeof process === 'undefined') { 4 | global.process = require('process') 5 | } else { 6 | const bProcess = require('process') 7 | for (var p in bProcess) { 8 | if (!(p in process)) { 9 | process[p] = bProcess[p] 10 | } 11 | } 12 | } 13 | 14 | process.browser = false 15 | if (typeof Buffer === 'undefined') global.Buffer = require('buffer').Buffer 16 | 17 | // global.location = global.location || { port: 80 } 18 | const isDev = typeof __DEV__ === 'boolean' && __DEV__ 19 | process.env['NODE_ENV'] = isDev ? 'development' : 'production' 20 | if (typeof localStorage !== 'undefined') { 21 | localStorage.debug = isDev ? '*' : '' 22 | } 23 | 24 | // If using the crypto shim, uncomment the following line to ensure 25 | // crypto is loaded first, so it can populate global.crypto 26 | // require('crypto') 27 | -------------------------------------------------------------------------------- /react-native-hub-app/src/helpers.tsx: -------------------------------------------------------------------------------- 1 | import { AsyncStorage } from 'react-native' 2 | import { ThreadID, PrivateKey } from '@textile/hub' 3 | 4 | const version = 10003 //Math.floor(Math.random() * 1000); 5 | const IDENTITY_KEY = 'identity-' + version 6 | const USER_THREAD_ID = 'user-thread-' + version 7 | 8 | export const cacheUserThread = async (id: ThreadID) => { 9 | await AsyncStorage.setItem(USER_THREAD_ID, id.toString()) 10 | } 11 | 12 | export const getCachedUserThread = async (): Promise => { 13 | /** 14 | * All storage should be scoped to the identity 15 | * 16 | * If the identity changes and you try to use an old database, 17 | * it will error due to not authorized. 18 | */ 19 | const idStr = await AsyncStorage.getItem(USER_THREAD_ID) 20 | if (idStr) { 21 | /** 22 | * Temporary hack to get ThreadID working in RN 23 | */ 24 | const id: ThreadID = ThreadID.fromString(idStr) 25 | return id 26 | } 27 | return undefined 28 | } 29 | 30 | export const generateIdentity = async (): Promise => { 31 | let idStr = await AsyncStorage.getItem(IDENTITY_KEY) 32 | if (idStr) { 33 | return PrivateKey.fromString(idStr) 34 | } else { 35 | const id = PrivateKey.fromRandom() 36 | idStr = id.toString() 37 | await AsyncStorage.setItem(IDENTITY_KEY, idStr) 38 | return id 39 | } 40 | } 41 | 42 | export const astronautSchema = { 43 | $id: 'https://example.com/astronaut.schema.json', 44 | $schema: 'http://json-schema.org/draft-07/schema#', 45 | title: 'Astronauts', 46 | type: 'object', 47 | required: ['_id'], 48 | properties: { 49 | _id: { 50 | type: 'string', 51 | description: "The instance's id.", 52 | }, 53 | firstName: { 54 | type: 'string', 55 | description: "The astronaut's first name.", 56 | }, 57 | lastName: { 58 | type: 'string', 59 | description: "The astronaut's last name.", 60 | }, 61 | missions: { 62 | description: 'Missions.', 63 | type: 'integer', 64 | minimum: 0, 65 | }, 66 | }, 67 | } 68 | 69 | export const createAstronaut = () => { 70 | return { 71 | _id: '', 72 | firstName: 'Buzz', 73 | lastName: 'Aldrin', 74 | missions: 2, 75 | } 76 | } 77 | 78 | export const generateWebpage = (title: string) => { 79 | return `${title}

${title}
` 80 | } 81 | -------------------------------------------------------------------------------- /react-native-hub-app/src/styles.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native' 2 | 3 | const values = { 4 | fontPlaceSize: 14, 5 | fontTimeSize: 10, 6 | smallIconSize: 30, 7 | fontTempSize: 12, 8 | } 9 | 10 | const styles = StyleSheet.create({ 11 | container: { 12 | marginTop: 14, 13 | alignSelf: 'stretch', 14 | }, 15 | list: { 16 | margin: 0, 17 | alignSelf: 'stretch', 18 | }, 19 | row: { 20 | elevation: 1, 21 | borderRadius: 2, 22 | backgroundColor: 'white', 23 | flex: 1, 24 | flexDirection: 'row', // main axis 25 | justifyContent: 'flex-start', // main axis 26 | alignItems: 'center', // cross axis 27 | paddingTop: 10, 28 | paddingBottom: 10, 29 | paddingLeft: 18, 30 | paddingRight: 16, 31 | marginLeft: 14, 32 | marginRight: 14, 33 | marginTop: 0, 34 | marginBottom: 6, 35 | }, 36 | rowCellTimeplace: { 37 | flex: 1, 38 | flexDirection: 'column', 39 | }, 40 | rowCellTemp: { 41 | color: '#777', 42 | paddingHorizontal: 16, 43 | flex: 0, 44 | fontSize: values.fontTempSize, 45 | }, 46 | rowTime: { 47 | color: '#777', 48 | textAlignVertical: 'bottom', 49 | includeFontPadding: false, 50 | flex: 0, 51 | fontSize: values.fontTimeSize, 52 | }, 53 | rowName: { 54 | color: '#333', 55 | textAlignVertical: 'top', 56 | includeFontPadding: false, 57 | flex: 0, 58 | fontSize: values.fontPlaceSize, 59 | }, 60 | error: { 61 | color: '#333', 62 | flex: 0, 63 | paddingTop: 24, 64 | textAlign: 'center', 65 | fontSize: values.fontPlaceSize, 66 | }, 67 | }) 68 | 69 | export default styles 70 | -------------------------------------------------------------------------------- /react-native-hub-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES6", 5 | "module": "commonjs", 6 | "allowJs": true, 7 | "resolveJsonModule": true, 8 | "jsx": "react", 9 | "noEmit": true, 10 | "skipLibCheck": true,/* Strict Type-Checking Options */ 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "suppressImplicitAnyIndexErrors": true,/* Additional Checks */ 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true,/* Module Resolution Options */ 23 | "moduleResolution": "node", 24 | "baseUrl": ".", 25 | "allowSyntheticDefaultImports": true, 26 | "esModuleInterop": true 27 | }, 28 | "exclude": [ 29 | "app/__tests__/" // exclude test files 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /scripts/outdated.sh: -------------------------------------------------------------------------------- 1 | for d in ./*/ ; do 2 | if [ $d != "./scripts/" ]; then 3 | (cd "$d" && npm run textile:check); 4 | fi; 5 | done 6 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | for d in ./*/ ; do 2 | if [ $d != "./scripts/" ]; then 3 | (cd "$d" && npm install); 4 | fi; 5 | done 6 | -------------------------------------------------------------------------------- /scripts/update.sh: -------------------------------------------------------------------------------- 1 | for d in ./*/ ; do 2 | if [ $d != "./scripts/" ]; then 3 | (cd "$d" && npm run textile:upgrade && npm install); 4 | fi; 5 | done 6 | -------------------------------------------------------------------------------- /simple-auth/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "standard" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": 12, 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /simple-auth/README.md: -------------------------------------------------------------------------------- 1 | ## Basic auth flows 2 | 3 | Basic auth flow example nodejs app showing the difference between secure and insecure keys. 4 | This is also a "pure" ES module app to showcase that workflow for Textile modules. 5 | 6 | ## Textile Hub key & secret 7 | 8 | This app uses your developer keys to access the Hub. You'll want to keep these secret! We use 9 | the `dotenv` package here to keep things safe. 10 | 11 | ``` 12 | mv example.env .env 13 | ``` 14 | 15 | Edit `.env`. add your own api key and secret from `hub keys ls`, using the Textile Hub CLI. 16 | You can also create new keys with `hub keys create`. Be sure to create secure keys for production 17 | apps. 18 | Leave `APP_IDENTITY` empty if you do not have a private key to use. The first time you run `index.js` a new identity will be created and stored there. 19 | 20 | ## App 21 | 22 | The app is intentionally simple. All the logic is contained in a single file. Since it is an ES 23 | module, in newer versions of nodejs, you should be able to run it with: 24 | 25 | ``` 26 | node index.js 27 | ``` 28 | -------------------------------------------------------------------------------- /simple-auth/example.env: -------------------------------------------------------------------------------- 1 | API=https://webapi.hub.textile.io 2 | APP_API_KEY= 3 | APP_API_SECRET= 4 | APP_IDENTITY= -------------------------------------------------------------------------------- /simple-auth/index.js: -------------------------------------------------------------------------------- 1 | import hub from '@textile/hub' 2 | import dotenv from 'dotenv' 3 | import fs from 'fs' 4 | 5 | // Read from .env file 6 | dotenv.config() 7 | 8 | // Default to hub api. 9 | const API = process.env.API || undefined 10 | // Pull out required modules 11 | const { Client, PrivateKey, createUserAuth } = hub 12 | 13 | /** 14 | * Create random identity for testing. 15 | * @param string The exported string of the user identity. If undefined will write new key to .env. 16 | */ 17 | function identity (string = undefined) { 18 | if (string) return PrivateKey.fromString(string) 19 | // Create a new one if this is the first time 20 | const id = PrivateKey.fromRandom() 21 | // Write it to the file for use next time 22 | fs.appendFileSync('.env', `APP_IDENTITY=${id.toString()}`) 23 | return id 24 | } 25 | 26 | /** 27 | * Main async function. 28 | * @param insecure Should be true if the user group key is an insecure key 29 | * (i.e., doesn't require auth signature), false otherwise. 30 | */ 31 | async function main (insecure = true) { 32 | let client 33 | if (insecure) { 34 | // If using insecure keys, we can stick with jut key info. 35 | client = await Client.withKeyInfo( 36 | { 37 | key: process.env.APP_API_KEY, 38 | secret: process.env.APP_API_SECRET 39 | }, 40 | API, 41 | process.env.NODE_ENV !== 'production' 42 | ) 43 | } else { 44 | // Create user auth object with auth signature valid for 30 minutes. 45 | client = Client.withUserAuth(await createUserAuth( 46 | process.env.APP_API_KEY, // User group key 47 | process.env.APP_API_SECRET // User group key secret 48 | ), 49 | API, 50 | process.env.NODE_ENV !== 'production' 51 | ) 52 | } 53 | // Now that we have the pertinent metadata, authenticate user with hub. 54 | await client.getToken(identity(process.env.APP_IDENTITY)) 55 | } 56 | 57 | // Run main async program. 58 | main(false) 59 | -------------------------------------------------------------------------------- /simple-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth-test", 3 | "version": "1.0.0", 4 | "description": "a tiny npm package for testing textile auth flows", 5 | "main": "index.js", 6 | "module": "index.js", 7 | "type": "module", 8 | "scripts": { 9 | "textile:check": "npx ncu '/^@textile/.*$/'", 10 | "textile:upgrade": "npx ncu -u '/^@textile/.*$/'" 11 | }, 12 | "keywords": [ 13 | "test", 14 | "auth", 15 | "textile", 16 | "security" 17 | ], 18 | "author": "Carson Farmer ", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "eslint": "^7.14.0", 22 | "eslint-config-standard": "^16.0.2", 23 | "eslint-plugin-import": "^2.22.1", 24 | "eslint-plugin-node": "^11.1.0", 25 | "eslint-plugin-promise": "^4.2.1" 26 | }, 27 | "dependencies": { 28 | "@textile/hub": "^6.0.1", 29 | "dotenv": "^8.2.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /user-mailbox-setup/README.md: -------------------------------------------------------------------------------- 1 | See full [README.md here](../README.md). 2 | 3 | Remember to edit `App.tsx` to include your Hub API key: 4 | 5 | ```javascript 6 | ... 7 | this.client = await Users.withKeyInfo({key: 'HUB API KEY HERE'}) 8 | ... 9 | ``` -------------------------------------------------------------------------------- /user-mailbox-setup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailbox-setup", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "11.0.0-next.12", 7 | "@textile/hub": "^6.0.1", 8 | "ethers": "^5.0.8", 9 | "react": "^16.13.1", 10 | "react-dom": "^16.13.1", 11 | "react-fontawesome": "^1.7.1", 12 | "react-scripts": "3.4.1", 13 | "slate-react-system": "^0.1.0" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^14.0.14", 17 | "@types/react": "^16.9.41", 18 | "@types/react-dom": "^16.9.8", 19 | "@types/react-fontawesome": "^1.6.4", 20 | "http-proxy-middleware": "^1.0.4", 21 | "typescript": "^3.9.5" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "eject": "react-scripts eject", 27 | "textile:check": "npx ncu '/^@textile/.*$/'", 28 | "textile:upgrade": "npx ncu -u '/^@textile/.*$/'" 29 | }, 30 | "eslintConfig": { 31 | "extends": "react-app" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /user-mailbox-setup/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/user-mailbox-setup/public/favicon.ico -------------------------------------------------------------------------------- /user-mailbox-setup/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /user-mailbox-setup/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/user-mailbox-setup/public/logo192.png -------------------------------------------------------------------------------- /user-mailbox-setup/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textileio/js-examples/a8a5a9fe8cf8331f0bc4f811791e15dbc0597469/user-mailbox-setup/public/logo512.png -------------------------------------------------------------------------------- /user-mailbox-setup/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /user-mailbox-setup/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /user-mailbox-setup/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // @ts-ignore 3 | import { ButtonPrimary, ButtonSecondary, ButtonDisabled, CardTabGroup, Input, Table } from 'slate-react-system' 4 | import { PrivateKey, Users, MailboxEvent, UserMessage } from '@textile/hub' 5 | 6 | /** Example identity only. Your users should have their own private keys */ 7 | const PrivateKeyIdentity = 'bbaareqhvbkss57bqe7u2jz3hmoitqntbbdflalqbnmtpm7t7yq7ee5rhyvre23iivcvttvguhghjt2ht4i2dan7zak7v7h55yblnd5cbwe3m2' 8 | 9 | /** 10 | * A simple type to hold inbox messages after they have been 11 | * decrypted with the PrivateKey 12 | */ 13 | interface DecryptedInbox { 14 | id: string 15 | body: string 16 | from: string 17 | sent: number 18 | readAt?: number 19 | } 20 | 21 | class App extends React.Component { 22 | state = { 23 | mailboxes: '1', // manages which tab is selected 24 | newMessage: '', // holds the message during composition 25 | inbox: Array(), // the list of all messages 26 | ready: false // app state 27 | } 28 | 29 | client?: Users // Our connected Users API client 30 | 31 | _handleChange = (e: any) => this.setState({ [e.target.name]: e.target.value }); 32 | 33 | componentDidMount = async () => { 34 | // Setup the user's PrivateKey identity 35 | const identity = PrivateKey.fromString(PrivateKeyIdentity) 36 | console.log('Your public identity:', identity.public.toString()) 37 | 38 | // Connect to the API with hub keys. 39 | // Use withUserAuth for production. 40 | this.client = await Users.withKeyInfo({key: 'HUB API KEY HERE'}) 41 | 42 | // Authorize the user to access your Huh api 43 | await this.client.getToken(identity) 44 | 45 | // Setup the user's mailbox 46 | const mailboxID = await this.client.setupMailbox() 47 | 48 | // Create a listener for all new messages in the inbox 49 | this.client.watchInbox(mailboxID, this.handleNewMessage) 50 | 51 | // Grab all existing inbox messages and decrypt them locally 52 | const messages = await this.client.listInboxMessages() 53 | const inbox = [] 54 | for (const message of messages) { 55 | inbox.push(await this.messageDecoder(message)) 56 | } 57 | 58 | this.setState({ready: true, inbox}) 59 | } 60 | 61 | /** 62 | * Decrypts a user's inbox messages using their PrivateKey 63 | */ 64 | messageDecoder = async (message: UserMessage): Promise => { 65 | const identity = PrivateKey.fromString(PrivateKeyIdentity) 66 | const bytes = await identity.decrypt(message.body) 67 | const body = new TextDecoder().decode(bytes) 68 | const {from} = message 69 | const {readAt} = message 70 | const {createdAt} = message 71 | const {id} = message 72 | return {body, from, readAt, sent: createdAt, id} 73 | } 74 | 75 | /** 76 | * Handles a new inbox listen event 77 | */ 78 | handleNewMessage = async (reply?: MailboxEvent, err?: Error) => { 79 | if (err) return 80 | if (!this.client) return 81 | if (!reply || !reply.message) return 82 | const message = await this.messageDecoder(reply.message) 83 | this.setState({ 84 | inbox: [...this.state.inbox, message], 85 | }) 86 | } 87 | 88 | /** 89 | * This example will simply send a message to yourself, instead of 90 | * creating two distinct users. 91 | */ 92 | sendMessageToSelf = async () => { 93 | if (!this.state.newMessage || this.state.newMessage === '' || !this.client) return 94 | const encoded = new TextEncoder().encode(this.state.newMessage) 95 | const identity = PrivateKey.fromString(PrivateKeyIdentity) 96 | await this.client.sendMessage(identity, identity.public, encoded) 97 | this.setState({newMessage: ''}) 98 | } 99 | 100 | /** 101 | * Remove the message from the inbox 102 | */ 103 | deleteMessage = async (id: string) => { 104 | if (!this.client) return 105 | await this.client.deleteInboxMessage(id) 106 | this.setState({ 107 | inbox: this.state.inbox.filter((msg) => msg.id !== id) 108 | }) 109 | } 110 | 111 | renderInbox = () => { 112 | const rows = this.state.inbox.reverse().map((message, i) => { 113 | return { 114 | ...message, 115 | key: i, 116 | delete: { 117 | // Deletes this message from the inbox 118 | this.deleteMessage(message.id) 119 | }}>x 120 | } 121 | }) 122 | return ( 123 | 124 |
125 | 137 | 138 |
139 | 140 | ) 141 | } 142 | 143 | renderSentbox = () => { 144 | return ( 145 |
146 | 153 | {this.state.ready ? Send : Waiting for API} 154 |
155 | ) 156 | } 157 | 158 | render() { 159 | const tabs = [ 160 | { value: "1", label: "Send" }, 161 | { value: "2", label: `Inbox (${this.state.inbox.length})` }, 162 | ] 163 | return ( 164 | 165 |
166 | 172 |
173 | {this.state.mailboxes === "1" ? this.renderSentbox() : this.renderInbox() } 174 |
175 |
176 |
177 | ) 178 | } 179 | } 180 | 181 | export default App; 182 | -------------------------------------------------------------------------------- /user-mailbox-setup/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | body { 5 | padding: 1% 3%; 6 | display: flex; 7 | min-height: 100vh; 8 | flex-direction: column; 9 | margin: 0; 10 | background: #333; 11 | } 12 | .container { 13 | width: 600px; 14 | height: 600px; 15 | background-color: #efeffd; 16 | position: fixed; 17 | left: 0; 18 | right: 0; 19 | top: 0; 20 | bottom: 0; 21 | margin: auto; 22 | max-width: 100%; 23 | max-height: 100%; 24 | overflow: auto; 25 | } 26 | .container > .tab { 27 | display: flex; 28 | flex-direction: column; 29 | justify-content: center; 30 | align-items: center; 31 | height: 300px; 32 | } 33 | .container > .tab > .compose { 34 | width: 300px; 35 | color: #aaaaff; 36 | } 37 | .container > .tab > .compose > button { 38 | margin-top: 15px; 39 | } 40 | .container > .tab > .inbox { 41 | width: 100%; 42 | color: #333; 43 | vertical-align: top; 44 | } 45 | .container > .tab > .filler { 46 | width: 100%; 47 | flex: 1; 48 | color: #333; 49 | vertical-align: top; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /user-mailbox-setup/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /user-mailbox-setup/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /user-mailbox-setup/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /user-mailbox-setup/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /user-mailbox-setup/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /user-mailbox-setup/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "downlevelIteration": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | 28 | --------------------------------------------------------------------------------