├── .gitignore ├── App.js ├── LICENSE ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── flash-circuit-512.png ├── icon.png └── splash.png ├── babel.config.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | /android 37 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | // Polyfill 2 | import "react-zlib-js"; 3 | 4 | import { useMemo, useEffect, useState, useCallback } from "react"; 5 | import { StatusBar } from "expo-status-bar"; 6 | import { StyleSheet, Image, Text, View, TextInput } from "react-native"; 7 | import bwipjs from "bwip-js"; 8 | import * as b64 from "base-64"; 9 | import { Buffer } from "buffer"; 10 | import { WebView } from "react-native-webview"; 11 | import * as OTPAuth from "otpauth"; 12 | import { useAsyncStorage } from "@react-native-async-storage/async-storage"; 13 | 14 | const TOTP_STEP = 15; 15 | 16 | const totp = (secret, now) => 17 | new OTPAuth.TOTP({ 18 | secret, 19 | period: TOTP_STEP, 20 | }).generate({ timestamp: now }); 21 | 22 | const unixNow = () => Math.floor(Date.now()); 23 | 24 | function generateSignedToken(ticket, time) { 25 | const parsedTicket = JSON.parse(Buffer.from(ticket, "base64").toString()); 26 | 27 | const bearerKey = parsedTicket.t; 28 | const customerKey = Buffer.from(parsedTicket.ck, "hex"); 29 | const eventKey = Buffer.from(parsedTicket.ek, "hex"); 30 | 31 | const eventOTP = totp(eventKey, time); 32 | const customerOTP = totp(customerKey, time); 33 | const secs = Math.floor(time / 1000); 34 | return [bearerKey, eventOTP, customerOTP, secs].join("::"); 35 | } 36 | 37 | function useSignedToken(ticket) { 38 | const [currentTime, setTime] = useState(0); 39 | const [currentCounter, setCounter] = useState(0); 40 | 41 | useEffect(() => { 42 | const interval = setInterval(() => { 43 | const newTime = unixNow(); 44 | const counter = Math.floor(newTime / (TOTP_STEP * 1000)); 45 | if (counter !== currentCounter) { 46 | setCounter(counter); 47 | setTime(newTime); 48 | } 49 | }, 500); 50 | return () => clearInterval(interval); 51 | }, [currentCounter]); 52 | 53 | const token = useMemo(() => { 54 | try { 55 | return generateSignedToken(ticket, currentTime); 56 | } catch (err) { 57 | return ""; 58 | } 59 | }, [ticket, currentTime]); 60 | 61 | return token; 62 | } 63 | 64 | function Barcode({ text }) { 65 | const html = useMemo(() => { 66 | const uri = 67 | "data:image/svg+xml;base64," + 68 | b64.encode( 69 | bwipjs 70 | .toSVG({ 71 | bcid: "pdf417", 72 | text, 73 | }) 74 | .trim(), 75 | ); 76 | return `
`; 77 | }, [text]); 78 | 79 | return ( 80 | 81 | 82 | 83 | ); 84 | } 85 | 86 | function Logo({ style }) { 87 | return ( 88 | 89 | ); 90 | } 91 | 92 | export default function App() { 93 | const [ticket, setTicket] = useState(""); 94 | 95 | const ticketStorage = useAsyncStorage("ticket"); 96 | useEffect(() => { 97 | ticketStorage.getItem((err, t) => { 98 | if (err) return console.error(err); 99 | if (t) setTicket(t); 100 | }); 101 | }, []); 102 | 103 | const updateTicket = useCallback( 104 | (newTicket) => { 105 | setTicket(newTicket); 106 | ticketStorage.setItem(newTicket).catch(console.error); 107 | }, 108 | [ticketStorage], 109 | ); 110 | 111 | const [ticketInputStyle, setTicketInputStyle] = useState( 112 | styles.ticketInputBlurred, 113 | ); 114 | const onFocusTicketInput = useCallback(() => { 115 | setTicketInputStyle(styles.ticketInputFocused); 116 | }, []); 117 | 118 | const onBlurTicketInput = useCallback(() => { 119 | setTicketInputStyle(styles.ticketInputBlurred); 120 | }, []); 121 | 122 | const token = useSignedToken(ticket); 123 | 124 | return ( 125 | 126 | 127 | TicketGimp 128 | 138 | {token ? : } 139 | 140 | 141 | ); 142 | } 143 | 144 | const styles = StyleSheet.create({ 145 | container: { 146 | flex: 1, 147 | paddingTop: 50, 148 | backgroundColor: "#080c18", 149 | alignItems: "center", 150 | }, 151 | logo: { 152 | position: "absolute", 153 | width: 40, 154 | height: 40, 155 | left: 25, 156 | top: 60, 157 | }, 158 | ticketInput: { 159 | padding: 5, 160 | borderRadius: 5, 161 | backgroundColor: "#222", 162 | borderBottomWidth: 1, 163 | width: "80%", 164 | color: "white", 165 | margin: 50, 166 | }, 167 | ticketInputFocused: { 168 | borderBottomWidth: 2, 169 | borderBottomColor: "#d500f9", 170 | }, 171 | ticketInputBlurred: { 172 | borderBottomWidth: 1, 173 | borderBottomColor: "#ddd", 174 | }, 175 | title: { 176 | flex: 1, 177 | color: "white", 178 | fontFamily: "monospace", 179 | fontSize: 40, 180 | }, 181 | barcode: { 182 | flex: 3, 183 | }, 184 | webview: { 185 | width: 350, 186 | flex: 0, 187 | height: 120, 188 | }, 189 | }); 190 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ticketgimp 2 | 3 | Create TicketMaster-compatible rotating barcodes from encoded ticket secrets. 4 | 5 | [See my full blog post](https://conduition.io/coding/ticketmaster/) for explanation and context. 6 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "ticketgimp", 4 | "slug": "ticketgimp", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#080c18" 12 | }, 13 | "assetBundlePatterns": [ 14 | "**/*" 15 | ], 16 | "ios": { 17 | "supportsTablet": true 18 | }, 19 | "android": { 20 | "adaptiveIcon": { 21 | "foregroundImage": "./assets/adaptive-icon.png", 22 | "backgroundColor": "#080c18" 23 | }, 24 | "package": "com.anonymous.ticketgimp" 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conduition/ticketgimp/08f527526b84a1e01dd889468e17fe96933f1b09/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conduition/ticketgimp/08f527526b84a1e01dd889468e17fe96933f1b09/assets/favicon.png -------------------------------------------------------------------------------- /assets/flash-circuit-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conduition/ticketgimp/08f527526b84a1e01dd889468e17fe96933f1b09/assets/flash-circuit-512.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conduition/ticketgimp/08f527526b84a1e01dd889468e17fe96933f1b09/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conduition/ticketgimp/08f527526b84a1e01dd889468e17fe96933f1b09/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ticketgimp", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start --offline --port 8981", 7 | "build": "expo prebuild -p android && cd android && ./gradlew assembleRelease", 8 | "format": "prettier --write App.js", 9 | "android": "expo run:android", 10 | "ios": "expo run:ios" 11 | }, 12 | "dependencies": { 13 | "@expo/metro-runtime": "~3.1.1", 14 | "base-64": "^1.0.0", 15 | "bwip-js": "^4.2.0", 16 | "expo": "~50.0.5", 17 | "expo-status-bar": "~1.11.1", 18 | "otpauth": "^9.2.2", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0", 21 | "react-native": "0.73.2", 22 | "react-native-web": "~0.19.6", 23 | "react-native-webview": "13.6.4", 24 | "react-zlib-js": "^1.0.5", 25 | "@react-native-async-storage/async-storage": "1.21.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.20.0", 29 | "prettier": "^3.2.4" 30 | }, 31 | "private": true 32 | } 33 | --------------------------------------------------------------------------------