├── .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 |
--------------------------------------------------------------------------------