# jwt_decoder_app **Repository Path**: gobkc/jwt_decoder_app ## Basic Information - **Project Name**: jwt_decoder_app - **Description**: jwt_decoder_app is a desktop app for the Linux platform. It is used to convert the payload part of a JWT string to JSON format. - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-29 - **Last Updated**: 2026-05-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # jwt_decoder_app A small Flutter **desktop** utility (Linux/Windows/macOS) that takes a JWT (JSON Web Token) string, extracts the **payload** segment, base64url-decodes it, and shows the result as **pretty-printed JSON** in a read-only multi-line text box. The app uses Material 3 in **dark mode**, including the OS window header bar on Linux (via `gtk-application-prefer-dark-theme`). --- ## Table of contents 1. [Screens / features](#screens--features) 2. [How a Flutter project is created (from scratch)](#how-a-flutter-project-is-created-from-scratch) 3. [Project layout — which files are code, which are generated](#project-layout) 4. [Source files used by this app](#source-files-used-by-this-app) 5. [Building, running, testing — via Makefile](#building-running-testing) 6. [Packaging as a `.deb`](#packaging-as-a-deb) 7. [Verifying the Debian package](#verifying-the-debian-package) --- ## Screens / features - Top text box: paste a JWT (`header.payload.signature`). - Buttons: - **Decode Payload** — base64url-decode the second segment, parse as JSON, pretty-print with 2-space indent. - **Paste** — pull JWT from the clipboard. - **Copy Result** — copy pretty JSON to the clipboard. - **Clear** — reset everything. - Bottom text box (read-only): pretty JSON output. - Errors (invalid format / not JSON) are shown in a red banner. --- ## How a Flutter project is created (from scratch) You need the Flutter SDK installed (`flutter --version` should print a 3.x version; this project was built against Flutter 3.44 / Dart 3.12). ```bash # 1. Pick a parent directory for your Flutter projects mkdir -p flutter_projects cd flutter_projects # 2. Create a new Flutter project that targets desktop platforms flutter create --platforms=linux,windows,macos jwt_decoder_app # 3. Enter the project, fetch dependencies cd jwt_decoder_app flutter pub get # 4. Run it on Linux desktop flutter run -d linux # 5. Build a release binary for Linux flutter build linux --release # output: build/linux/x64/release/bundle/jwt_decoder_app + data/ + lib/ ``` `flutter create` generates ~60 files. The handful that are actually **source code you edit** are listed below. Everything else is either generated, build output, or platform scaffolding. --- ## Project layout ``` jwt_decoder_app/ ├── Makefile ← build / package targets (this repo adds) ├── README.md ← this file ├── pubspec.yaml ← Dart/Flutter dependency manifest (code) ├── analysis_options.yaml ← lint config (code) │ ├── lib/ ← Dart application code │ └── main.dart ← ★ the entire app lives here │ ├── test/ ← Dart test code │ └── widget_test.dart ← smoke test │ ├── assets/ ← static assets (this repo adds) │ └── jwt_decoder_app.svg ← ★ app logo (used by .desktop file) │ ├── packaging/ ← packaging metadata (this repo adds) │ └── jwt_decoder_app.desktop ← ★ XDG desktop entry installed by the .deb │ ├── linux/ ← Linux desktop runner (C++/CMake; partially generated) │ ├── CMakeLists.txt │ ├── flutter/ ← generated by flutter tool — do not edit │ └── runner/ │ ├── main.cc │ ├── my_application.cc ← ★ edited to force GTK dark theme │ ├── my_application.h │ └── ... │ ├── windows/ ← Windows runner (generated; only edit if you need to) ├── macos/ ← macOS runner (generated; only edit if you need to) │ ├── build/ ← generated build artifacts (gitignore) ├── .dart_tool/ ← generated tool state (gitignore) ├── dist/ ← release artifacts created by `make package` / `make deb` └── pubspec.lock ← generated lockfile (commit it) ``` ### What counts as "code" in this project Files you actually wrote / edited (the ★ items above): | File | Purpose | | --------------------------------------------- | -------------------------------------------------------------- | | `lib/main.dart` | All Dart UI + decode logic | | `test/widget_test.dart` | Widget smoke test | | `pubspec.yaml` | Dependency manifest | | `analysis_options.yaml` | Lint rules | | `linux/runner/my_application.cc` | Linux GTK runner — patched to prefer dark theme and set title | | `assets/jwt_decoder_app.svg` | App icon (also shipped to `/usr/share/icons/...` by the `.deb`) | | `packaging/jwt_decoder_app.desktop` | Desktop entry (installed by the `.deb` so the app appears in app search) | | `Makefile` | Build / package targets | | `README.md` | This document | Everything else under `linux/flutter/`, `windows/`, `macos/`, `.dart_tool/`, `build/`, and most of `linux/runner/` was generated by `flutter create` and is not normally edited by hand. --- ## Source files used by this app The full source listing follows. Re-creating these files inside a fresh `flutter create --platforms=linux,windows,macos jwt_decoder_app` is enough to reproduce the application. ### `lib/main.dart` ```dart import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; void main() { runApp(const JwtDecoderApp()); } class JwtDecoderApp extends StatelessWidget { const JwtDecoderApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'JWT Payload Decoder', debugShowCheckedModeBanner: false, themeMode: ThemeMode.dark, darkTheme: ThemeData( brightness: Brightness.dark, colorScheme: ColorScheme.fromSeed( seedColor: Colors.indigo, brightness: Brightness.dark, ), useMaterial3: true, ), home: const JwtDecoderHomePage(), ); } } class JwtDecoderHomePage extends StatefulWidget { const JwtDecoderHomePage({super.key}); @override State createState() => _JwtDecoderHomePageState(); } class _JwtDecoderHomePageState extends State { final TextEditingController _jwtController = TextEditingController(); final TextEditingController _payloadController = TextEditingController(); String _errorMessage = ''; @override void dispose() { _jwtController.dispose(); _payloadController.dispose(); super.dispose(); } String _normalizeBase64(String input) { // JWT uses base64url encoding without padding String output = input.replaceAll('-', '+').replaceAll('_', '/'); switch (output.length % 4) { case 0: break; case 2: output += '=='; break; case 3: output += '='; break; default: throw const FormatException('Invalid base64url string'); } return output; } void _decodeJwt() { setState(() { _errorMessage = ''; _payloadController.text = ''; }); final raw = _jwtController.text.trim(); if (raw.isEmpty) { setState(() { _errorMessage = 'Please paste a JWT string first.'; }); return; } final parts = raw.split('.'); if (parts.length < 2) { setState(() { _errorMessage = 'Invalid JWT format. A JWT should contain at least header.payload sections separated by ".".'; }); return; } try { final normalized = _normalizeBase64(parts[1]); final decodedBytes = base64.decode(normalized); final decodedStr = utf8.decode(decodedBytes); final jsonObj = jsonDecode(decodedStr); const encoder = JsonEncoder.withIndent(' '); final pretty = encoder.convert(jsonObj); setState(() { _payloadController.text = pretty; }); } catch (e) { setState(() { _errorMessage = 'Failed to decode JWT payload: $e'; }); } } void _clearAll() { setState(() { _jwtController.clear(); _payloadController.clear(); _errorMessage = ''; }); } Future _copyResult() async { if (_payloadController.text.isEmpty) return; await Clipboard.setData(ClipboardData(text: _payloadController.text)); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Payload JSON copied to clipboard'), duration: Duration(seconds: 2), ), ); } Future _pasteJwt() async { final data = await Clipboard.getData(Clipboard.kTextPlain); if (data?.text != null) { setState(() { _jwtController.text = data!.text!; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('JWT Payload Decoder'), backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, foregroundColor: Theme.of(context).colorScheme.onSurface, elevation: 0, ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'JWT String', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Expanded( flex: 2, child: TextField( controller: _jwtController, maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, style: const TextStyle( fontFamily: 'monospace', fontSize: 13, ), decoration: const InputDecoration( hintText: 'Paste your JWT here (e.g. eyJhbGciOi...xxx.yyy.zzz)', border: OutlineInputBorder(), alignLabelWithHint: true, ), ), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ ElevatedButton.icon( onPressed: _decodeJwt, icon: const Icon(Icons.lock_open), label: const Text('Decode Payload'), ), OutlinedButton.icon( onPressed: _pasteJwt, icon: const Icon(Icons.paste), label: const Text('Paste'), ), OutlinedButton.icon( onPressed: _copyResult, icon: const Icon(Icons.copy), label: const Text('Copy Result'), ), TextButton.icon( onPressed: _clearAll, icon: const Icon(Icons.clear), label: const Text('Clear'), ), ], ), if (_errorMessage.isNotEmpty) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade900.withValues(alpha: 0.3), border: Border.all(color: Colors.red.shade400), borderRadius: BorderRadius.circular(6), ), child: Row( children: [ Icon(Icons.error_outline, color: Colors.red.shade300), const SizedBox(width: 8), Expanded( child: Text( _errorMessage, style: TextStyle(color: Colors.red.shade200), ), ), ], ), ), ], const SizedBox(height: 16), const Text( 'Decoded Payload (formatted JSON)', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Expanded( flex: 3, child: TextField( controller: _payloadController, maxLines: null, expands: true, readOnly: true, textAlignVertical: TextAlignVertical.top, style: const TextStyle( fontFamily: 'monospace', fontSize: 13, ), decoration: const InputDecoration( hintText: 'The decoded payload JSON will appear here.', border: OutlineInputBorder(), alignLabelWithHint: true, filled: true, ), ), ), ], ), ), ); } } ``` ### `test/widget_test.dart` ```dart // Basic smoke test for the JWT Decoder app. import 'package:flutter_test/flutter_test.dart'; import 'package:jwt_decoder_app/main.dart'; void main() { testWidgets('JWT Decoder app smoke test', (WidgetTester tester) async { await tester.pumpWidget(const JwtDecoderApp()); expect(find.text('JWT Payload Decoder'), findsOneWidget); expect(find.text('Decode Payload'), findsOneWidget); }); } ``` ### `linux/runner/my_application.cc` — edits vs. the generated file Two changes vs. what `flutter create` produced: 1. In `my_application_startup`, force the GTK dark theme so the OS-drawn header bar / window decoration matches the Flutter UI: ```c GtkSettings* settings = gtk_settings_get_default(); if (settings != nullptr) { g_object_set(settings, "gtk-application-prefer-dark-theme", TRUE, nullptr); } ``` 2. In `my_application_activate`, the window title is set to `"JWT Payload Decoder"` instead of the default `"jwt_decoder_app"` in both the GNOME header bar branch and the fallback `gtk_window_set_title` branch. ### `assets/jwt_decoder_app.svg` A hand-written SVG logo (256×256, scalable). Shipped to `/usr/share/icons/hicolor/scalable/apps/jwt_decoder_app.svg` by the `.deb`. ### `packaging/jwt_decoder_app.desktop` ```ini [Desktop Entry] Type=Application Name=JWT Payload Decoder GenericName=JWT Decoder Comment=Decode and pretty-print the payload of a JWT token Exec=jwt_decoder_app Icon=jwt_decoder_app Terminal=false Categories=Utility;Development; Keywords=jwt;json;token;decoder;base64; StartupWMClass=jwt_decoder_app ``` The `Icon=jwt_decoder_app` line is the part that links to the SVG icon (the desktop spec resolves it by looking up `jwt_decoder_app.svg` in the hicolor icon theme). --- ## Building, running, testing All actions are driven through `make` targets defined in `Makefile`: ```bash make help # list all targets make # alias for `make release` make deps # flutter pub get make analyze # flutter analyze (static analysis) make test # flutter test make run # flutter run -d linux (debug, hot reload) make debug # build/linux/x64/debug/bundle/ make profile # build/linux/x64/profile/bundle/ make release # build/linux/x64/release/bundle/ make package # dist/jwt_decoder_app-linux-x64.tar.gz make deb # dist/jwt-decoder-app_1.0.0_amd64.deb make install # install to ~/.local (override PREFIX=... if you want) make clean # flutter clean + remove dist/ ``` The release Linux binary lives at: ``` build/linux/x64/release/bundle/jwt_decoder_app build/linux/x64/release/bundle/data/ ← required runtime data build/linux/x64/release/bundle/lib/ ← required runtime libs ``` Keep the whole `bundle/` directory together — the executable will not run without the sibling `data/` and `lib/` directories. --- ## Packaging as a `.deb` `make deb` produces a Debian package at: ``` dist/jwt-decoder-app_1.0.0_amd64.deb ``` The package layout is: ``` /usr/bin/jwt_decoder_app → symlink to /usr/lib/.../jwt_decoder_app /usr/lib/jwt_decoder_app/ ← release bundle (binary + data/ + lib/) /usr/share/applications/jwt_decoder_app.desktop ← XDG desktop entry /usr/share/icons/hicolor/scalable/apps/jwt_decoder_app.svg ← SVG logo ``` Why this layout? - `Exec=jwt_decoder_app` in the `.desktop` file resolves through `/usr/bin/jwt_decoder_app`. - `Icon=jwt_decoder_app` is resolved by the icon theme system, which looks for `jwt_decoder_app.svg` under `/usr/share/icons/hicolor/scalable/apps/`. - That gives you a clickable launcher in GNOME / KDE / Cinnamon / whatever app menu, **with the SVG logo** visible in search results. Debian package metadata (in `DEBIAN/control`): - Package name: `jwt-decoder-app` (Debian package names cannot contain `_`, so the package is hyphenated; the binary / desktop file / icon all stay as `jwt_decoder_app`). - Architecture: `amd64`. - Depends: `libgtk-3-0` (the GTK3 runtime that Flutter Linux apps need). - `postinst` / `postrm` scripts run `update-desktop-database` and `gtk-update-icon-cache` so the new entry shows up immediately. ### Install / uninstall ```bash sudo dpkg -i dist/jwt-decoder-app_1.0.0_amd64.deb # After install, open the activities/app overview and type "JWT" — # you should see the "JWT Payload Decoder" entry with the SVG logo. # Or launch from a terminal: jwt_decoder_app # Remove later: sudo dpkg -r jwt-decoder-app ``` --- ## Verifying the Debian package You can inspect the produced `.deb` without installing it: ```bash # Metadata dpkg-deb -I dist/jwt-decoder-app_1.0.0_amd64.deb # Full contents listing dpkg-deb -c dist/jwt-decoder-app_1.0.0_amd64.deb # Confirm the SVG is in the right place dpkg-deb -c dist/jwt-decoder-app_1.0.0_amd64.deb | grep '\.svg' # → ./usr/share/icons/hicolor/scalable/apps/jwt_decoder_app.svg ``` After `sudo dpkg -i`, GNOME / KDE / etc. will discover the app via the `.desktop` file in `/usr/share/applications/` and the icon via the hicolor theme — typing the app name in the app overview will surface the entry with the SVG logo.