About Services Process Projects Code Contact
Available for projects

Quality.
Code. Design.

I'm Will Fisher — a multi-disciplined technologist delivering sharp QA, clean code, and thoughtful design for products that need to work and look great.

50+ Projects Delivered
3 Core Disciplines
100% Client Focused

Built to ship great products

From test suites to pixel-perfect UIs — I cover the full stack of delivery.

I'm a versatile professional with deep roots in Quality Assurance, a passion for clean, purposeful code, and an eye for design that actually converts. I work across the entire product lifecycle.

Whether you need bulletproof test coverage, a feature built from scratch, or a UI redesign that users will love — I bring precision and intent to every stage of the process.

I believe great products are built at the intersection of reliability, functionality, and beauty. That's what I deliver.

Selenium Java Python PHP HTML5 Figma CI/CD Postman Agile
// wfcodes.profile.json
 
$ whoami
→ WFCodes
 
$ cat roles.txt
→ QA Engineer
→ Software Developer
→ UI/UX Designer
 
$ status --availability
→ Open to new projects ✓
 
$ git log --oneline -3
→ feat: ship pixel-perfect UI
→ fix: zero bug escape rate
→ chore: refactor for scale
 
$ _

What I bring to your project

Three focused disciplines. End-to-end capability.

🧪

Quality Assurance

Systematic test strategies that catch bugs before users do. From manual exploratory testing to fully automated regression suites, I ensure your software ships with confidence.

Test Automation Regression Testing Performance QA Bug Triage Test Plans
💻

Development

Clean, maintainable code that solves real problems. I build features, integrations, and full applications with a focus on scalability, readability, and performance.

Frontend Dev Database Design CI/CD Pipelines Code Review
🎨

Design

Interfaces that feel as good as they look. I design user experiences grounded in clarity and purpose — wireframes, prototypes, and polished visual systems ready for handoff.

UI Design UX Research Prototyping Design Systems Accessibility Brand Identity
📋

Technical Consulting

Strategic guidance on tooling, process, and architecture. I help teams build the right systems and workflows to move faster without sacrificing quality.

QA Strategy Tech Audits Process Design Tooling Setup
🔗

Integrations & Automation

Connect your tools and automate repetitive workflows. I build reliable integrations between platforms, reducing manual effort and keeping your data in sync.

Webhooks Workflow Automation Scripting
🚀

Project Rescue

Struggling with a stalled build, a fragile codebase, or a product falling apart under load? I step in, diagnose the issues, and get things back on track.

Code Audit Debt Reduction Bug Squashing Stabilization

A process built for clarity

No guesswork. No surprises. Just steady progress toward your goal.

01

Discovery Call

We talk through your goals, constraints, and what success looks like for your project.

02

Proposal & Scope

I put together a clear plan with deliverables, timeline, and pricing. No ambiguity.

03

Execution

Work begins with regular updates through whatever channel works best for you — Slack, email, phone calls. You're always in the loop without being in the weeds.

04

Review & Deliver

We review together, refine as needed, and I hand over everything clean and documented.

Things I've built

Personal and independent projects shipped outside of client work.

Longbox
Android

Longbox

Your comic collection, organised and always with you.

Longbox is an Android app built for comic book collectors. Track your entire physical collection, manage a wishlist, and browse metadata from the Grand Comics Database — all without needing an account or an internet connection. Your library lives on your device, private and offline-first by design.

Android Kotlin SQLite Google AdMob Offline-first

🧪 Want to help test Longbox before its full release?

Sign up here →
Longbox screenshot 1 — library overview Longbox screenshot 2 — catalog lookup Longbox screenshot 3 — barcode scan Longbox screenshot 4 — add comic details Longbox screenshot 5 — collection stats
Game Wall
Android

Game Wall

Your game library, organized and always on hand.

Game Wall is a video game collection tracker for Android. Build a personal library and wishlist with cover art and platform details, search IGDB to add titles fast, and browse collection stats at a glance — all stored on your device, no account required.

Android IGDB API Google AdMob Offline-first

🧪 Want to help test Game Wall before its full release?

Sign up here →
Game Wall screenshot 1 — library Game Wall screenshot 2 — search IGDB Game Wall screenshot 3 — collection stats Game Wall screenshot 4 — settings

A look at the code

Real snippets pulled straight from projects I've built — not cherry-picked boilerplate.

contact.php PHP · wfcodes.com

The backend handler behind this site's contact form — sanitizes input, verifies reCAPTCHA server-side with Google before touching anything else, then relays to Formspree with a graceful fallback message if delivery fails.

<?php
require_once 'includes/config.php';

header('Content-Type: application/json');

// ── Only accept POST ───────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['success' => false, 'message' => 'Method not allowed.']);
    exit;
}

// ── Collect & sanitize inputs ──────────────────────────────────────
$name    = trim(strip_tags($_POST['name']    ?? ''));
$email   = trim(strip_tags($_POST['email']   ?? ''));
$service = trim(strip_tags($_POST['service'] ?? ''));
$message = trim(strip_tags($_POST['message'] ?? ''));
$captcha = $_POST['g-recaptcha-response'] ?? '';

// ── Basic validation ───────────────────────────────────────────────
$errors = [];

if (empty($name))              $errors[] = 'Name is required.';
if (empty($email))             $errors[] = 'Email is required.';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Please enter a valid email address.';
if (empty($message))           $errors[] = 'Message is required.';
if (empty($captcha))           $errors[] = 'Please complete the CAPTCHA.';

if (!empty($errors)) {
    http_response_code(422);
    echo json_encode(['success' => false, 'message' => implode(' ', $errors)]);
    exit;
}

// ── Verify reCAPTCHA with Google ───────────────────────────────────
$recaptcha_url  = 'https://www.google.com/recaptcha/api/siteverify';
$recaptcha_data = [
    'secret'   => RECAPTCHA_SECRET_KEY,
    'response' => $captcha,
    'remoteip' => $_SERVER['REMOTE_ADDR'] ?? ''
];

$recaptcha_options = [
    'http' => [
        'method'  => 'POST',
        'header'  => 'Content-Type: application/x-www-form-urlencoded',
        'content' => http_build_query($recaptcha_data)
    ]
];

$recaptcha_context  = stream_context_create($recaptcha_options);
$recaptcha_response = file_get_contents($recaptcha_url, false, $recaptcha_context);
$recaptcha_result   = json_decode($recaptcha_response, true);

if (empty($recaptcha_result['success'])) {
    http_response_code(422);
    echo json_encode(['success' => false, 'message' => 'CAPTCHA verification failed. Please try again.']);
    exit;
}

// ── Forward to Formspree ───────────────────────────────────────────
$form_data = http_build_query([
    'name'     => $name,
    'email'    => $email,
    'service'  => $service ?: 'Not specified',
    'message'  => $message,
    '_replyto' => $email,
    '_subject' => 'New enquiry via WFCodes.com — ' . $name,
]);

$formspree_options = [
    'http' => [
        'method'  => 'POST',
        'header'  => implode("\r\n", [
            'Content-Type: application/x-www-form-urlencoded',
            'Accept: application/json',
            'Referer: ' . SITE_URL
        ]),
        'content' => $form_data
    ]
];

$formspree_context  = stream_context_create($formspree_options);
$formspree_response = @file_get_contents(FORMSPREE_ENDPOINT, false, $formspree_context);
$formspree_result   = json_decode($formspree_response, true);

// ── Respond to client ──────────────────────────────────────────────
if ($formspree_response !== false && isset($formspree_result['ok'])) {
    http_response_code(200);
    echo json_encode(['success' => true, 'message' => 'Message sent! I\'ll be in touch within 24 hours.']);
} else {
    // Formspree failed — log it server-side for visibility
    error_log('[WFCodes contact] Formspree submission failed for ' . $email . ' at ' . date('Y-m-d H:i:s'));
    http_response_code(500);
    echo json_encode([
        'success' => false,
        'message' => 'Something went wrong on our end. Please email ' . CONTACT_EMAIL . ' directly.'
    ]);
}
exit;
GameCard.kt Kotlin · Game Wall

Two reusable Jetpack Compose components from Game Wall — a library card that adapts to missing cover art with a letter-tile fallback, and a shared screen header used across Library, Wishlist, and Stats.

@Composable
fun GameCard(
    game: Game,
    modifier: Modifier = Modifier,
    showWishlistPriority: Boolean = false
) {
    Card(
        modifier = modifier.fillMaxWidth(),
        colors = CardDefaults.cardColors(containerColor = Surface2Dark),
        shape = RoundedCornerShape(12.dp)
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(12.dp),
            horizontalArrangement = Arrangement.spacedBy(12.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            if (game.coverImageUrl.isNotBlank()) {
                AsyncImage(
                    model = IgdbMapper.upgradeCoverUrl(game.coverImageUrl),
                    contentDescription = game.title,
                    modifier = Modifier
                        .size(56.dp, 72.dp)
                        .clip(RoundedCornerShape(8.dp))
                        .background(MaterialTheme.colorScheme.surface),
                    contentScale = ContentScale.Crop
                )
            } else {
                androidx.compose.foundation.layout.Box(
                    modifier = Modifier
                        .size(56.dp, 72.dp)
                        .clip(RoundedCornerShape(8.dp))
                        .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        text = game.title.take(1).uppercase(),
                        style = MaterialTheme.typography.titleLarge,
                        color = MaterialTheme.colorScheme.primary
                    )
                }
            }
            Column(modifier = Modifier.weight(1f)) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Text(
                        text = game.title,
                        style = MaterialTheme.typography.titleMedium,
                        fontWeight = FontWeight.SemiBold,
                        color = TextPrimary,
                        maxLines = 2,
                        overflow = TextOverflow.Ellipsis,
                        modifier = Modifier.weight(1f, fill = false)
                    )
                    if (showWishlistPriority && game.wishlistPriority) {
                        Icon(
                            imageVector = Icons.Filled.Star,
                            contentDescription = "Priority",
                            tint = AccentGold,
                            modifier = Modifier.padding(start = 4.dp)
                        )
                    }
                    if (game.isHighlight) {
                        Icon(
                            imageVector = Icons.Filled.Star,
                            contentDescription = "Highlight",
                            tint = AccentGold,
                            modifier = Modifier.padding(start = 4.dp)
                        )
                    }
                }
                if (game.subtitleLabel().isNotBlank()) {
                    Text(
                        text = game.subtitleLabel(),
                        style = MaterialTheme.typography.bodySmall,
                        color = TextMuted,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis
                    )
                }
                if (game.playStatus != com.gamewall.app.domain.model.PlayStatus.UNPLAYED) {
                    Text(
                        text = game.playStatus.label,
                        style = MaterialTheme.typography.labelSmall,
                        color = TextMuted
                    )
                }
            }
        }
    }
}

@Composable
fun MainScreenHeader(
    title: String,
    subtitle: String,
    onSettingsClick: () -> Unit,
    modifier: Modifier = Modifier,
    showSettings: Boolean = true
) {
    Row(
        modifier = modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.Top
    ) {
        Column(modifier = Modifier.weight(1f)) {
            Text(
                text = title,
                style = MaterialTheme.typography.headlineMedium,
                fontWeight = FontWeight.Bold,
                color = TextPrimary
            )
            Text(
                text = subtitle,
                style = MaterialTheme.typography.bodyMedium,
                color = TextMuted
            )
        }
        if (showSettings) {
            IconButton(onClick = onSettingsClick) {
                Icon(
                    imageVector = Icons.Filled.Settings,
                    contentDescription = "Settings",
                    tint = TextPrimary
                )
            }
        }
    }
}
LevelEditorScene.java Java · Fimbulwinter Engine

Core rendering setup from Fimbulwinter, a custom 2D game engine built on LWJGL/OpenGL — hand-written GLSL shaders, and raw VAO/VBO/EBO buffer management to get a colored quad on screen without a higher-level framework in the way.

public class LevelEditorScene extends Scene {

    private String vertexShaderSrc = "#version 330 core\n" +
            "layout (location=0) in vec3 aPos;\n" +
            "layout (location=1) in vec4 aColor;\n" +
            "\n" +
            "out vec4 fColor;\n" +
            "\n" +
            "void main(){\n" +
            "    fColor = aColor;\n" +
            "    gl_Position = vec4(aPos, 1.0);\n" +
            "}";

    private String fragmentShaderSrc = "#version 330 core\n" +
            "\n" +
            "in vec4 fColor;\n" +
            "\n" +
            "out vec4 color;\n" +
            "\n" +
            "void main (){\n" +
            "    color = fColor;\n" +
            "}";

    //private int vertexID, fragmentID, shaderProgram;

    //sets the color for each vertex
    private float[] vertexArray = {
            //position          //color
            0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f, 1.0f, //bottom right  0
            -0.5f, 0.5f, 0.0f,  0.0f, 1.0f, 0.0f, 1.0f, //top left      1
            0.5f, 0.5f, 0.0f,   0.0f,0.0f, 1.0f, 1.0f,  //top right     2
            -0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, //bottom left   3
    };

    //IMPORTANT: Must be in counter-clockwise order
    //Sets the location for each triangle
    private int[] elementArray = {
            2,1,0, //top right triangle
            0,1,3, //bottom left triangle
    };

    private int vaoID, vboID, eboID;
    private  Shader defaultShader;

    public LevelEditorScene(){

    }

    @Override
    public void init() {

        defaultShader = new Shader("assets/shaders/default.glsl");
        defaultShader.compile();

        //Generating the VAO, VBO, and EBO buffer objects, and send them to the GPU
        vaoID = glGenVertexArrays();
        glBindVertexArray(vaoID);

        //create a float buffer of vertices
        FloatBuffer vertexBuffer = BufferUtils.createFloatBuffer(vertexArray.length);
        vertexBuffer.put(vertexArray).flip();

        //create VBO upload the vertex buffer
        vboID = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vboID);
        glBufferData(GL_ARRAY_BUFFER, vertexBuffer, GL_STATIC_DRAW);

        //create the indices and upload
        IntBuffer elementBuffer = BufferUtils.createIntBuffer(elementArray.length);
        elementBuffer.put(elementArray).flip();

        eboID = glGenBuffers();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboID);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, elementBuffer, GL_STATIC_DRAW);

        //add the vertex attribute pointers
        int positionsSize = 3;
        int colorSize = 4;
        int floatSizeBytes = 4;
        int vertexSizeBytes = (positionsSize + colorSize) * floatSizeBytes;

        glVertexAttribPointer(0, positionsSize, GL_FLOAT, false, vertexSizeBytes, 0);
        glEnableVertexAttribArray(0);

        glVertexAttribPointer(1, colorSize, GL_FLOAT, false, vertexSizeBytes, positionsSize * floatSizeBytes);
        glEnableVertexAttribArray(1);
    }

    @Override
    public void update(float dt) {
        defaultShader.use();

        //bind the VAO
        glBindVertexArray(vaoID);

        //enable the vertex attribute pointers
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);

        glDrawElements(GL_TRIANGLES, elementArray.length, GL_UNSIGNED_INT, 0);

        //unbind everything
        glDisableVertexAttribArray(0);
        glEnableVertexAttribArray(1);

        glBindVertexArray(0);

        defaultShader.detach();
    }

}

Ready to build something?

Drop me a message and I'll get back to you within 24 hours.