Search by

noehassiel / signature-pad

noehassiel

Native signature capture for NativePHP Mobile — a freehand drawing surface (SwiftUI Path on iOS, Compose Canvas on Android) that hands PHP a base64-encoded PNG.

Package info

github.com/noehassiel/signature-pad

Language:Kotlin

Type:nativephp-plugin

pkg:composer/noehassiel/signature-pad

Statistics

Installs: 69

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.3 2026-08-28 16:02 UTC

This package is auto-updated.

Last update: 2026-09-09 18:54:04 UTC


README

Native signature capture for NativePHP Mobile v4. Ships a new EDGE element, <native:signature-pad>, that renders a freehand drawing surface and hands PHP the drawing as a base64-encoded PNG data URL.

Why this exists

EDGE cannot capture a signature. <native:canvas> is a declarative container for shape primitives — rects, circles, lines — with no touch-path capture and no raster export, and there is no drawing element in core or in any published plugin. That makes a signature a genuinely new element type, which is exactly what the UI component plugin mechanism is for. (The NativePHP docs use "a signature pad" as their own example of the case.)

The wire format is a plain data:image/png;base64,... string — the same thing a web <canvas> signature pad posts — so an existing server endpoint that already accepts signatures from a web form needs no changes to accept one from a phone.

Installation

From Packagist:

composer require noehassiel/signature-pad

Or as a local path package while developing:

"repositories": [
    { "type": "path", "url": "./packages/noehassiel/signature-pad" }
]

Then, either way:

php artisan vendor:publish --tag=nativephp-plugins-provider   # once, before the first plugin
php artisan native:plugin:register noehassiel/signature-pad
php artisan native:plugin:list                                # verify it appears
php artisan native:plugin:validate                            # verify the manifest

Then rebuildphp artisan native:run ios or android. The native renderers are copied in and registered by generated code at build time, so a newly added component needs a build, not just a composer update. Adding the plugin and only restarting PHP gives you an element that serializes fine and renders nothing.

Requirements

  • nativephp/mobile ^4.0
  • Android: minSdk 26. No permissions.
  • iOS: 16.0. No Info.plist entries.

No permissions of any kind — it draws on a surface the app already owns.

Usage

<native:signature-pad
    ref="inspection-item-{{ $item['id'] }}"
    _change="signatureChanged"
    :clear-token="$clearToken"
    :value="$existingSignature"
    pen-color="#111827"
    :stroke-width="2.5"
    :read-only="$accepted"
    class="w-full h-40 rounded-xl border border-theme-outline bg-theme-surface" />
class InspectionScreen extends NativeComponent
{
    public string $signature = '';
    public int $clearToken = 0;

    /** Fires on stroke end with the whole drawing so far. */
    public function signatureChanged(string $dataUrl): void
    {
        $this->signature = $dataUrl;
    }

    public function clearSignature(): void
    {
        $this->clearToken++;      // any change wipes the pad
        $this->signature = '';
    }

    public function save(): void
    {
        // A plain base64 PNG — post it wherever your web signature pad posts.
        Api::post("inspections/{$id}/items/{$itemId}/response", [
            'value' => ['value' => 'signed'],
            'signature' => $this->signature,
        ]);
    }
}

Attributes

Attribute Type Default Notes
ref string node id Stable id, required if you use capture()
_change method Called on stroke end with the full drawing as a data URL
value string|null An existing signature to display
pen-color string #111827 Any color the Tailwind parser accepts
stroke-width float 2.5 dp / points
read-only bool false Shows ink, ignores touches
clear-token int|string Change it to wipe the pad
undo-token int|string Change it to drop the last stroke

Tailwind classes on the tag are parsed by the same parser core elements use, so sizing, radius, and background need nothing from this plugin.

Emit, don't poll

_change fires on stroke end carrying the entire drawing, and that is the path to use: the screen holds the latest PNG, so a save button reads state it already has. This matters for offline work — by the time a queued write drains, the pad may be long gone, so the image has to be in the payload, not fetched later.

SignaturePad::capture() exists for the case where the control that needs the signature cannot see the pad's state (a shell-level action bar, or a sheet owning the button while a child owns the pad). Reach for it second.

use Noehassiel\SignaturePad\Facades\SignaturePad;
use Noehassiel\SignaturePad\Events\SignatureCaptured;
use Native\Mobile\Attributes\On;

SignaturePad::capture('inspection-item-9');

#[On(SignatureCaptured::class)]
public function signatureReady(string $ref, string $signature, bool $isEmpty): void
{
    if ($isEmpty) {
        return; // an untouched pad still exports a valid blank PNG
    }

    $this->signature = $signature;
}

Usage (JavaScript / Inertia)

The element itself is native and cannot be drawn into from a SPA frontend — what the JS module exposes is the bridge function, for pulling the drawing out of a pad a native screen is showing.

import { capture, Events } from 'noehassiel-signature-pad';

await capture('inspection-item-9');

// The drawing arrives as an event, not a return value.
on(Events.SignatureCaptured, ({ ref, signature, isEmpty }) => {
    if (! isEmpty) {
        upload(signature); // data:image/png;base64,...
    }
});

Clearing is a token, not a call

PHP has no imperative channel into a mounted view, so clear-token / undo-token are props the renderer watches: change the value and the pad reacts. It survives re-renders, needs no view handle, and cannot get out of sync with component state.

Events

Event Payload When
SignatureCaptured ref, signature, isEmpty Reply to capture()
SignatureCaptureFailed ref, reason capture() named a ref no mounted pad claims — usually the sheet closed

Stroke updates do not go through an event; they use the element's _change callback.

Always check isEmpty. A pad nobody touched still exports a valid white PNG, so a non-empty string does not mean somebody signed.

Implementation notes

  • iOS is not PencilKit. PKCanvasView brings a tool picker, ruler, and its own undo stack, its ink is pressure- and tilt-reactive in ways that make a finger signature look nothing like a pen one, and flattening a PKDrawing to PNG means fighting its image pipeline. A signature is one pen, one colour, one flat raster — a Path over a DragGesture is less code and closer to what the web pad produces.
  • Exports are white-backed, not transparent. Signatures get stamped onto reports, and transparent ink becomes black-on-black in a dark PDF viewer.
  • Export runs on stroke end, never on move. A signature is hundreds of move events; one PNG each would flood the bridge.
  • Fixed 2× export scale so a phone and a tablet post comparable images.
  • A tap with no drag still leaves a dot — one-point strokes are drawn as circles, on screen and in the export.
  • NativeUINode.id is an Int, not a string — found by an actual build failure (cannot convert value of type 'Int' to expected argument type 'String'). Any fallback that uses it as a ref/key needs String(node.id) (Swift) / node.id.toString() (Kotlin).
  • getCallbackId() and getColor() return non-optional Int, not Int?/a color object — 0 is the "absent" sentinel for a callback id, and the color is a packed ARGB Int you unpack yourself (UIColor(argb:) on iOS, Color(Int) on Android, which accepts one directly). if let cb = onChangeCb doesn't compile against a non-optional Int; check onChangeCb != 0 instead — same reason Kotlin needs != 0, not != null.
  • sendTextChangeEvent's third parameter is labeled text:, not value:, on both platforms.

Testing

The PHP half — manifest, element registration, props, callback ids — is straightforward to cover with NativePHP's component testing: render a screen containing the element and assert the published node.

Native::test(SignatureScreen::class)
    ->assertElement('signature_pad', fn (array $node) => is_int($node['props']['on_change'] ?? null));

The renderers themselves are native and need a real device — a simulator will draw, but confirm export size and stroke-end latency on hardware.

License

Proprietary. All rights reserved © noehassiel. See LICENSE. Redistribution, resale, or republishing of this package — in source or compiled form — is not permitted without prior written permission from the copyright holder.