diff --git a/README.md b/README.md
index ed01e21..f7ecb45 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
The MIT-licensed SDK for official [InvoiceShelf](https://invoiceshelf.com) v3 modules. It extends [`nwidart/laravel-modules`](https://github.com/nWidart/laravel-modules) with the contracts used by InvoiceShelf's module host and signed marketplace packages.
-SDK **3.5** supports Module API **1.4**. First-party module packages live in their own repositories and are licensed `AGPL-3.0-only`; this SDK remains MIT.
+SDK **3.7** supports Module API **1.6**. First-party module packages live in their own repositories and are licensed `AGPL-3.0-only`; this SDK remains MIT.
## What a module can add
@@ -12,6 +12,7 @@ SDK **3.5** supports Module API **1.4**. First-party module packages live in the
- Actions, panels, form sections and save hooks on host records (invoices, estimates, customers, payments and more), plus entries in the global New menu.
- Company abilities contributed to the host's authorization catalogue, so a module can ship its own permissions instead of reusing host ones.
- Whole invoices to read and structured XML to embed in invoice PDFs (Module API 1.5.0), for modules that produce e-invoices; see [Documents and invoice PDFs](#documents-and-invoice-pdfs).
+- QR codes drawn by the host (Module API 1.6.0), so a module needs no QR library of its own; see [QR codes](#qr-codes).
- Narrow host contracts under [`src/Contracts/Host`](src/Contracts/Host) for settings, authorization, and read-only company-data queries. `CompanyDataReader` covers the AI assistant's built-in queries plus `companyMembers()` and `existingInvoiceIds()` for modules that assign work to members or stamp their entries against invoices. Module code must not depend on InvoiceShelf Eloquent models.
- AI drivers through [`src/Ai`](src/Ai): extend `AiDriver`, return `AiChatResponse`, and throw `AiException` for safe, localizable provider failures.
@@ -23,8 +24,8 @@ Declare compatibility in `module.json`, then test it against the host versions y
| Concern | Current contract |
| --- | --- |
-| SDK | `invoiceshelf/modules` `^3.6` |
-| Module API | `^1.3.0`; `^1.4.0` for bundled dependencies and record slots; `^1.5.0` for documents and invoice PDF attachments |
+| SDK | `invoiceshelf/modules` `^3.7` |
+| Module API | `^1.3.0`; `^1.4.0` for bundled dependencies and record slots; `^1.5.0` for documents and invoice PDF attachments; `^1.6.0` for QR codes |
| Host application | Your explicit InvoiceShelf 3.x range |
| PHP | The range your module actually supports |
@@ -253,6 +254,24 @@ A module that turns invoices into other documents, such as an EN 16931 e-invoice
These are Module API **1.5.0** additions; declare `"module_api": "^1.5.0"` to use them.
+### QR codes
+
+Ask the container for `QrCodeGenerator` instead of bundling a QR library:
+
+```php
+use InvoiceShelf\Modules\Contracts\Host\QrCodeGenerator;
+
+$src = app(QrCodeGenerator::class)->svgDataUri($payload, size: 160);
+//
+```
+
+- **`svg()`** returns an SVG document and **`svgDataUri()`** the same as a `data:` URI for an `
` in a PDF template or a Blade view. Both PDF drivers draw it without fetching anything, and no image extension is needed on the server.
+- **Content** is UTF-8 text. Plain ASCII is encoded as it is; anything else is encoded as UTF-8 with an ECI marker, which is what EPC (SEPA credit transfer) and Swiss QR-bill payloads expect.
+- **Options:** `$size` is the edge in pixels with the quiet zone included, `$margin` the quiet zone in modules (4 by default, as the QR standard asks), and `ErrorCorrection` ([`src/Qr`](src/Qr)) runs from `Low` to `High`, `Medium` by default, which the payment specifications above require.
+- Content that is empty or too long for a QR code at the chosen level throws `InvalidArgumentException`.
+
+These are Module API **1.6.0** additions; declare `"module_api": "^1.6.0"` to use them.
+
### Responsive classes
Your stylesheet is a Tailwind build of its own, loaded next to the host's, and the two share class names: both may define `.hidden` or `.w-full`. Between rules of equal weight the later sheet wins, so the host adds its stylesheet again after the module sheets, and a module can never break the app's layout. The cost lands on your markup: the host's plain utilities win over your breakpoint variants. `hidden sm:flex` stays hidden on desktop unless the host's own build happens to contain `sm:flex` too, and that changes from release to release.
diff --git a/RELEASING.md b/RELEASING.md
index 680a626..f9a46b3 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -18,7 +18,7 @@ An official module normally calls the tagged SDK workflow from its own tag workf
```yaml
jobs:
release:
- uses: InvoiceShelf/modules/.github/workflows/module-release.yml@3.6.0
+ uses: InvoiceShelf/modules/.github/workflows/module-release.yml@3.7.0
with:
channel: stable
secrets: inherit
diff --git a/src/Contracts/Host/QrCodeGenerator.php b/src/Contracts/Host/QrCodeGenerator.php
new file mode 100644
index 0000000..b1ee435
--- /dev/null
+++ b/src/Contracts/Host/QrCodeGenerator.php
@@ -0,0 +1,54 @@
+` in a PDF template: the renderer never fetches anything.
+ *
+ * @param int $size the edge of the square image in pixels, quiet zone included
+ * @param int $margin the quiet zone around the code, in modules; 4 is what the QR standard asks for
+ *
+ * @throws InvalidArgumentException when the content is empty or too long for a QR code at that level
+ */
+ public function svgDataUri(
+ string $content,
+ int $size = 200,
+ ErrorCorrection $errorCorrection = ErrorCorrection::Medium,
+ int $margin = 4,
+ ): string;
+}
diff --git a/src/Qr/ErrorCorrection.php b/src/Qr/ErrorCorrection.php
new file mode 100644
index 0000000..e87245e
--- /dev/null
+++ b/src/Qr/ErrorCorrection.php
@@ -0,0 +1,27 @@
+ $method->getName(),
+ (new ReflectionClass(QrCodeGenerator::class))->getMethods(),
+ );
+ sort($methods);
+
+ $this->assertSame(['svg', 'svgDataUri'], $methods);
+
+ foreach ($methods as $name) {
+ $method = new ReflectionMethod(QrCodeGenerator::class, $name);
+
+ $this->assertSame('string', (string) $method->getReturnType());
+ $this->assertSame(
+ ['content' => 'string', 'size' => 'int', 'errorCorrection' => ErrorCorrection::class, 'margin' => 'int'],
+ array_combine(
+ array_map(static fn ($parameter): string => $parameter->getName(), $method->getParameters()),
+ array_map(static fn ($parameter): string => (string) $parameter->getType(), $method->getParameters()),
+ ),
+ );
+ $this->assertSame([200, ErrorCorrection::Medium, 4], array_map(
+ static fn ($parameter) => $parameter->getDefaultValue(),
+ array_slice($method->getParameters(), 1),
+ ));
+ }
+ }
+
+ public function test_error_correction_levels_carry_the_standard_letters(): void
+ {
+ $this->assertSame(
+ ['Low' => 'L', 'Medium' => 'M', 'Quartile' => 'Q', 'High' => 'H'],
+ array_combine(
+ array_map(static fn (ErrorCorrection $level): string => $level->name, ErrorCorrection::cases()),
+ array_map(static fn (ErrorCorrection $level): string => $level->value, ErrorCorrection::cases()),
+ ),
+ );
+ }
+}