Compare commits

...
Author SHA1 Message Date
Jeppe B de780d857c Add POS card payment VAT visual previews 2026-07-27 18:05:53 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fd31609cb3 Bump ruby/setup-ruby from 1.319.0 to 1.321.0 (#226)
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.319.0
to 1.321.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ruby/setup-ruby/releases">ruby/setup-ruby's
releases</a>.</em></p>
<blockquote>
<h2>v1.321.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Add jruby-10.1.1.0 by <a
href="https://github.com/ruby-builder-bot"><code>@​ruby-builder-bot</code></a>
in <a
href="https://redirect.github.com/ruby/setup-ruby/pull/932">ruby/setup-ruby#932</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/setup-ruby/compare/v1.320.0...v1.321.0">https://github.com/ruby/setup-ruby/compare/v1.320.0...v1.321.0</a></p>
<h2>v1.320.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update CRuby releases on Windows by <a
href="https://github.com/ruby-builder-bot"><code>@​ruby-builder-bot</code></a>
in <a
href="https://redirect.github.com/ruby/setup-ruby/pull/931">ruby/setup-ruby#931</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/setup-ruby/compare/v1.319.0...v1.320.0">https://github.com/ruby/setup-ruby/compare/v1.319.0...v1.320.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ruby/setup-ruby/commit/95ef2b042f9d7a56d8268cba8559e2842e2ad01b"><code>95ef2b0</code></a>
Add jruby-10.1.1.0</li>
<li><a
href="https://github.com/ruby/setup-ruby/commit/a30dfa457ad68707b8b910ac3a244714b61c0626"><code>a30dfa4</code></a>
Update CRuby releases on Windows</li>
<li>See full diff in <a
href="https://github.com/ruby/setup-ruby/compare/003a5c4d8d6321bd302e38f6f0ec593f77f06600...95ef2b042f9d7a56d8268cba8559e2842e2ad01b">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.319.0&new-version=1.321.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 11:38:49 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 009519ee62 Bump actions/download-artifact from 4.3.0 to 8.0.1 (#227)
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 4.3.0 to 8.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Support for CJK characters in the artifact name by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/471">actions/download-artifact#471</a></li>
<li>Add a regression test for artifact name + content-type mismatches by
<a href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a>
in <a
href="https://redirect.github.com/actions/download-artifact/pull/472">actions/download-artifact#472</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v8...v8.0.1">https://github.com/actions/download-artifact/compare/v8...v8.0.1</a></p>
<h2>v8.0.0</h2>
<h2>v8 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/download-artifact@v8 has been migrated to an ESM module. This
should be transparent to the caller but forks might need to make
significant changes.</p>
</blockquote>
<blockquote>
<p>[!IMPORTANT]
Hash mismatches will now error by default. Users can override this
behavior with a setting change (see below).</p>
</blockquote>
<h3>Direct downloads</h3>
<p>To support direct uploads in <code>actions/upload-artifact</code>,
the action will no longer attempt to unzip all downloaded files.
Instead, the action checks the <code>Content-Type</code> header ahead of
unzipping and skips non-zipped files. Callers wishing to download a
zipped file as-is can also set the new <code>skip-decompress</code>
parameter to <code>true</code>.</p>
<h3>Enforced checks (breaking)</h3>
<p>A previous release introduced digest checks on the download. If a
download hash didn't match the expected hash from the server, the action
would log a warning. Callers can now configure the behavior on mismatch
with the <code>digest-mismatch</code> parameter. To be secure by
default, we are now defaulting the behavior to <code>error</code> which
will fail the workflow run.</p>
<h3>ESM</h3>
<p>To support new versions of the @actions/* packages, we've upgraded
the package to ESM.</p>
<h2>What's Changed</h2>
<ul>
<li>Don't attempt to un-zip non-zipped downloads by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/460">actions/download-artifact#460</a></li>
<li>Add a setting to specify what to do on hash mismatch and default it
to <code>error</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/461">actions/download-artifact#461</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v7...v8.0.0">https://github.com/actions/download-artifact/compare/v7...v8.0.0</a></p>
<h2>v7.0.0</h2>
<h2>v7 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/download-artifact@v7 now runs on Node.js 24 (<code>runs.using:
node24</code>) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.</p>
</blockquote>
<h3>Node.js 24</h3>
<p>This release updates the runtime to Node.js 24. v6 had preliminary
support for Node 24, however this action was by default still running on
Node.js 20. Now this action by default will run on Node.js 24.</p>
<h2>What's Changed</h2>
<ul>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/download-artifact/pull/440">actions/download-artifact#440</a></li>
<li>Download Artifact Node24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/415">actions/download-artifact#415</a></li>
<li>fix: update <code>@​actions/artifact</code> to fix Node.js 24
punycode deprecation by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/451">actions/download-artifact#451</a></li>
<li>prepare release v7.0.0 for Node.js 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/452">actions/download-artifact#452</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/download-artifact/commit/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c"><code>3e5f45b</code></a>
Add regression tests for CJK characters (<a
href="https://redirect.github.com/actions/download-artifact/issues/471">#471</a>)</li>
<li><a
href="https://github.com/actions/download-artifact/commit/e6d03f67377d4412c7aa56a8e2e4988e6ec479dd"><code>e6d03f6</code></a>
Add a regression test for artifact name + content-type mismatches (<a
href="https://redirect.github.com/actions/download-artifact/issues/472">#472</a>)</li>
<li><a
href="https://github.com/actions/download-artifact/commit/70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3"><code>70fc10c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/461">#461</a>
from actions/danwkennedy/digest-mismatch-behavior</li>
<li><a
href="https://github.com/actions/download-artifact/commit/f258da9a506b755b84a09a531814700b86ccfc62"><code>f258da9</code></a>
Add change docs</li>
<li><a
href="https://github.com/actions/download-artifact/commit/ccc058e5fbb0bb2352213eaec3491e117cbc4a5c"><code>ccc058e</code></a>
Fix linting issues</li>
<li><a
href="https://github.com/actions/download-artifact/commit/bd7976ba57ecea96e6f3df575eb922d11a12a9fd"><code>bd7976b</code></a>
Add a setting to specify what to do on hash mismatch and default it to
<code>error</code></li>
<li><a
href="https://github.com/actions/download-artifact/commit/ac21fcf45e0aaee541c0f7030558bdad38d77d6c"><code>ac21fcf</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/460">#460</a>
from actions/danwkennedy/download-no-unzip</li>
<li><a
href="https://github.com/actions/download-artifact/commit/15999bff51058bc7c19b50ebbba518eaef7c26c0"><code>15999bf</code></a>
Add note about package bumps</li>
<li><a
href="https://github.com/actions/download-artifact/commit/974686ed5098c7f9c9289ec946b9058e496a2561"><code>974686e</code></a>
Bump the version to <code>v8</code> and add release notes</li>
<li><a
href="https://github.com/actions/download-artifact/commit/fbe48b1d2756394be4cd4358ed3bc1343b330e75"><code>fbe48b1</code></a>
Update test names to make it clearer what they do</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/download-artifact/compare/d3f86a106a0bac45b974a628896c90dbdf5c8093...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=4.3.0&new-version=8.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 11:15:19 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b6f9b5a3a4 Bump actions/setup-node from 5 to 7 (#228)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5
to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-node/releases">actions/setup-node's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Add cache-primary-key and cache-matched-key as outputs by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1577">actions/setup-node#1577</a></li>
<li>Migrate to ESM and upgrade dependencies by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1574">actions/setup-node#1574</a></li>
</ul>
<h3>Bug fixes:</h3>
<ul>
<li>Remove dummy NODE_AUTH_TOKEN export by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1558">actions/setup-node#1558</a></li>
<li>Only use <code>mirrorToken</code> in <code>getManifest</code> if
it's provided by <a
href="https://github.com/deiga"><code>@​deiga</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
</ul>
<h3>Documentation updates:</h3>
<ul>
<li>Add documentation for publishing to npm with Trusted Publisher
(OIDC) by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li>docs: Update restore-only cache documentation by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1550">actions/setup-node#1550</a></li>
<li>docs: Update caching recommendations to mitigate cache poisoning
risks by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1567">actions/setup-node#1567</a></li>
</ul>
<h3>Dependency update:</h3>
<ul>
<li>Upgrade <code>@​actions/cache</code> to 5.1.0, log cache write
denied by <a
href="https://github.com/jasongin"><code>@​jasongin</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li><a href="https://github.com/deiga"><code>@​deiga</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
<li><a href="https://github.com/jasongin"><code>@​jasongin</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6...v7.0.0">https://github.com/actions/setup-node/compare/v6...v7.0.0</a></p>
<h2>v6.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update <code>@​actions/cache</code> to 5.1.0 and add security
overrides for undici and fast-xml-parser by <a
href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1579">actions/setup-node#1579</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0">https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0</a></p>
<h2>v6.4.0</h2>
<h2>What's Changed</h2>
<h3>Dependency updates:</h3>
<ul>
<li>Upgrade <a
href="https://github.com/actions"><code>@​actions</code></a>
dependencies by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li>
<li>Update Node.js versions in versions.yml and bump package to v6.4.0
by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1533">actions/setup-node#1533</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6...v6.4.0">https://github.com/actions/setup-node/compare/v6...v6.4.0</a></p>
<h2>v6.3.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Support parsing <code>devEngines</code> field by <a
href="https://github.com/susnux"><code>@​susnux</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1283">actions/setup-node#1283</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-node/commit/820762786026740c76f36085b0efc47a31fe5020"><code>8207627</code></a>
Migrate to ESM and upgrade dependencies (<a
href="https://redirect.github.com/actions/setup-node/issues/1574">#1574</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/04be95cf3511ea51ebf9f224ddfb99cc7ab87cd4"><code>04be95c</code></a>
Add cache-primary-key and cache-matched-key as outputs (<a
href="https://redirect.github.com/actions/setup-node/issues/1577">#1577</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/7c2c68d20d402ed6a201ada70a81341941093140"><code>7c2c68d</code></a>
docs: Update caching recommendations to mitigate cache poisoning risks
(<a
href="https://redirect.github.com/actions/setup-node/issues/1567">#1567</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/6a61c0375d66246de94630495909f12cf8dac84d"><code>6a61c03</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/setup-node/issues/1569">#1569</a>
from jasongin/update-actions-cache-5.1.0</li>
<li><a
href="https://github.com/actions/setup-node/commit/30eb73b41ded577900c1ebf968ef95cdf8f7434f"><code>30eb73b</code></a>
Resolve high-severity audit issues</li>
<li><a
href="https://github.com/actions/setup-node/commit/4e1a87a501d0302f99e30e2748568adcb388d09f"><code>4e1a87a</code></a>
Update dist</li>
<li><a
href="https://github.com/actions/setup-node/commit/360237f0c01778d0c17291f75c56d6feae4f7574"><code>360237f</code></a>
Strict equality</li>
<li><a
href="https://github.com/actions/setup-node/commit/4f8aac5beb2f0854bc79651567a18c67eb0b9de3"><code>4f8aac5</code></a>
Bump <code>@​actions/cache</code> to 5.1.0, log cache write denied</li>
<li><a
href="https://github.com/actions/setup-node/commit/f4a67bbeca970f103397d3d2b9462cf787cd2980"><code>f4a67bb</code></a>
Only use <code>mirrorToken</code> in <code>getManifest</code> if it's
provided (<a
href="https://redirect.github.com/actions/setup-node/issues/1548">#1548</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/0355742c943ddb13ca8a6b700f824231caa91e75"><code>0355742</code></a>
Remove dummy NODE_AUTH_TOKEN export (<a
href="https://redirect.github.com/actions/setup-node/issues/1558">#1558</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-node/compare/v5...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=5&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 12:55:41 +02:00
Jeppe B 7387a2b56e fix(i18n): unblock Truckwash iOS release catalog checks (#225)
Fix the location-permission source catalog nesting and regenerate all active v2 locale files so the App Store release matrix resolves the new compatibility aliases correctly.
2026-07-23 20:22:47 +02:00
Jeppe B 9ff103d4d0 fix(ios): respect status area and persist location consent (#224)
Keep the iOS status bar outside the Capacitor web view and replace startup geolocation watching with silent permission checks plus an explicit location action.

Verified by full unit, App Store readiness, Qodana, production build, Capacitor sync, and Playwright mobile suites.
2026-07-23 19:29:54 +02:00
39 changed files with 761 additions and 56 deletions
+2 -2
View File
@@ -32,12 +32,12 @@ jobs:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Setup Ruby
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
with:
ruby-version: "3.3"
@@ -32,7 +32,7 @@ jobs:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
@@ -125,13 +125,13 @@ jobs:
persist-credentials: false
- name: Setup Ruby and pinned Fastlane
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
with:
ruby-version: "3.3"
bundler-cache: true
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
+1 -1
View File
@@ -163,7 +163,7 @@ jobs:
echo "XCODE_VERSION=$xcode_version" >> "$GITHUB_ENV"
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
+2 -2
View File
@@ -167,13 +167,13 @@ jobs:
echo "IOS_SDK_VERSION=$sdk_version" >> "$GITHUB_ENV"
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
- name: Setup Ruby and pinned Fastlane
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
with:
ruby-version: "3.3"
bundler-cache: true
+1 -1
View File
@@ -102,7 +102,7 @@ jobs:
- name: Setup Node.js
if: steps.release-guard.outputs.current == 'true'
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
+3 -3
View File
@@ -68,7 +68,7 @@ jobs:
- name: Setup Node.js
if: steps.branch-head.outputs.current == 'true'
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
@@ -191,7 +191,7 @@ jobs:
ref: ${{ env.RELEASE_COMMIT_SHA }}
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
@@ -218,7 +218,7 @@ jobs:
run: node scripts/install-playwright-browsers.mjs chromium
- name: Download validated release package
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ needs.build-release.outputs.artifact_name }}
path: release-artifacts
+5 -5
View File
@@ -83,7 +83,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
@@ -125,7 +125,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
@@ -226,7 +226,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
@@ -436,7 +436,7 @@ jobs:
echo "head=$head_ref" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
@@ -662,7 +662,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
+7
View File
@@ -7,6 +7,13 @@ const config: CapacitorConfig = {
server: {
androidScheme: "https",
},
plugins: {
StatusBar: {
overlaysWebView: false,
style: "LIGHT",
backgroundColor: "#FFFFFFFF",
},
},
};
export default config;
+4 -2
View File
@@ -12,7 +12,8 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.4.1"),
.package(name: "CapacitorGeolocation", path: "../../../node_modules/@capacitor/geolocation")
.package(name: "CapacitorGeolocation", path: "../../../node_modules/@capacitor/geolocation"),
.package(name: "CapacitorStatusBar", path: "../../../node_modules/@capacitor/status-bar")
],
targets: [
.target(
@@ -20,7 +21,8 @@ let package = Package(
dependencies: [
.product(name: "Capacitor", package: "capacitor-swift-pm"),
.product(name: "Cordova", package: "capacitor-swift-pm"),
.product(name: "CapacitorGeolocation", package: "CapacitorGeolocation")
.product(name: "CapacitorGeolocation", package: "CapacitorGeolocation"),
.product(name: "CapacitorStatusBar", package: "CapacitorStatusBar")
]
)
]
+9
View File
@@ -12,6 +12,7 @@
"@bubblewrap/cli": "^1.24.1",
"@capacitor/core": "^8.4.1",
"@capacitor/geolocation": "^8.2.0",
"@capacitor/status-bar": "^8.0.3",
"@creativebulma/bulma-badge": "^1.0.1",
"@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.17",
@@ -2132,6 +2133,14 @@
"@capacitor/core": "^8.4.0"
}
},
"node_modules/@capacitor/status-bar": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.3.tgz",
"integrity": "sha512-csSpfNeN49Hx9JaQBSJEIiEbOLtXg3kcc2IpScq2fu5L520h3AWEvsxoH8Srk1jxfRKepaJ4S4sqSC3foI4AgA==",
"peerDependencies": {
"@capacitor/core": ">=8.0.0"
}
},
"node_modules/@capacitor/synapse": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz",
+1
View File
@@ -84,6 +84,7 @@
"@bubblewrap/cli": "^1.24.1",
"@capacitor/core": "^8.4.1",
"@capacitor/geolocation": "^8.2.0",
"@capacitor/status-bar": "^8.0.3",
"@creativebulma/bulma-badge": "^1.0.1",
"@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.17",
@@ -25,7 +25,6 @@ import {
import { PosSearchResult } from "./objects/PosSearchResult.vue";
import RegistrationNumberSearchResult from "@/components/models/pos/step1/RegistrationNumberSearchResult.vue";
import PosDepartmentStepMobileFixedBottomControl from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileFixedBottomControl.vue";
import PosDepartmentStepMobile1Location from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1Location.vue";
import PosDepartmentStepMobile1Debug from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1Debug.vue";
import PosDepartmentStepMobileAttachments from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileAttachments.vue";
import PosDepartmentStep1MobileTransactionHistory from "@/components/displays/department/pos/steps/mobile/views/PosDepartmentStep1MobileTransactionHistory.vue";
@@ -778,8 +777,6 @@ watch(
</div>
<!-- Debug -->
<PosDepartmentStepMobile1Debug v-if="debug_mode" :results="debug_request_results" />
<!-- Location -->
<PosDepartmentStepMobile1Location />
<!-- Buttons -->
<PosDepartmentStepMobileFixedBottomControl variant="pos-step" :use-backdrop-blur="views.attachmentView.value">
<!-- Attachments -->
@@ -1,68 +1,139 @@
<script setup lang="ts">
import { useGeolocation } from "@vueuse/core";
import { watch } from "vue";
import { computed, onMounted, ref } from "vue";
import {
getCurrentDeviceLocation,
type DeviceLocationErrorCode,
type DeviceLocationPermission,
} from "@/services/deviceLocation";
import { locations } from "../objects/PosDepartmentStepMobileFlow.vue";
const props = withDefaults(defineProps<{
enableHighAccuracy?: boolean;
maximumAge?: number;
timeout?: number;
autoLocateIfGranted?: boolean;
showPermissionControl?: boolean;
}>(), {
enableHighAccuracy: true,
maximumAge: 30000,
timeout: 27000,
autoLocateIfGranted: true,
showPermissionControl: true,
});
const emits = defineEmits<{
"location-updated": [coords: { latitude: number | null; longitude: number | null }];
"permission-state-changed": [permission: DeviceLocationPermission];
"location-error": [error: { code: DeviceLocationErrorCode; message: string | null }];
}>();
const { coords, locatedAt, error, resume, pause } = useGeolocation({
enableHighAccuracy: props.enableHighAccuracy,
maximumAge: props.maximumAge,
timeout: props.timeout,
});
const permission = ref<DeviceLocationPermission | null>(null);
const errorCode = ref<DeviceLocationErrorCode | null>(null);
const errorMessage = ref<string | null>(null);
const isChecking = ref(false);
const isRequesting = ref(false);
const getLocationTimestamp = () => {
const timestamp = Number(locatedAt.value);
return Number.isFinite(timestamp) ? timestamp : Date.now();
};
const shouldShowControl = computed(
() => props.showPermissionControl && (permission.value !== "granted" || errorCode.value !== null)
);
const canRequestPermission = computed(() => permission.value === "prompt");
const isBusy = computed(() => isChecking.value || isRequesting.value);
const onUpdate = (newCoords: { latitude: number | null; longitude: number | null }) => {
const normalizedCoords = locations.normalizeCoordinatePair(newCoords);
const updateLocation = (coords: { latitude: number; longitude: number }, timestamp: number) => {
const normalizedCoords = locations.normalizeCoordinatePair(coords);
if (normalizedCoords) {
const timestamp = getLocationTimestamp();
locations.set({
coords: normalizedCoords,
timestamp: new Date(timestamp),
locatedAt: timestamp,
errorMessage: error.value ? error.value.message : null,
})
emits('location-updated', normalizedCoords);
errorMessage: null,
});
emits("location-updated", normalizedCoords);
}
};
watch(coords, (newCoords) => {
onUpdate(newCoords);
const locate = async (promptIfNeeded: boolean) => {
if (isBusy.value) return;
errorCode.value = null;
errorMessage.value = null;
isChecking.value = !promptIfNeeded;
isRequesting.value = promptIfNeeded;
try {
const result = await getCurrentDeviceLocation({
promptIfNeeded,
enableHighAccuracy: props.enableHighAccuracy,
maximumAge: props.maximumAge,
timeout: props.timeout,
});
permission.value = result.permission;
emits("permission-state-changed", result.permission);
if (result.coords && result.timestamp !== null) {
updateLocation(result.coords, result.timestamp);
}
if (result.errorCode) {
errorCode.value = result.errorCode;
errorMessage.value = result.errorMessage;
emits("location-error", {
code: result.errorCode,
message: result.errorMessage,
});
}
} finally {
isChecking.value = false;
isRequesting.value = false;
}
};
const onLocationAction = () => locate(canRequestPermission.value);
onMounted(() => {
if (props.autoLocateIfGranted) {
locate(false);
}
});
</script>
<template>
<div v-show="false">
<p>Latitude: {{ coords.latitude }}</p>
<p>Longitude: {{ coords.longitude }}</p>
<p>Located At: {{ new Date(locatedAt).toLocaleString() }}</p>
<p v-if="error">Error: {{ error.message }}</p>
<template v-if="false">
<button @click="resume">Resume</button>
<button @click="pause">Pause</button>
<button @click="onUpdate({
latitude: 55.613448,
longitude: 12.495521
})">Update Now</button>
</template>
<div
v-if="shouldShowControl"
class="location-permission notification is-light"
data-testid="location-permission-control"
>
<p class="has-text-weight-semibold">{{ $t("location_permission.title") }}</p>
<p v-if="permission === 'denied'" class="is-size-7" data-testid="location-permission-denied">
{{ $t("location_permission.denied") }}
</p>
<p v-else-if="permission === 'unavailable'" class="is-size-7" data-testid="location-permission-unavailable">
{{ $t("location_permission.unavailable") }}
</p>
<p v-else-if="errorCode === 'timeout'" class="is-size-7" data-testid="location-permission-timeout">
{{ $t("location_permission.timeout") }}
</p>
<p v-else class="is-size-7">{{ $t("location_permission.explanation") }}</p>
<button
type="button"
class="button is-link is-fullwidth mt-3"
:class="{ 'is-loading': isBusy }"
:disabled="isBusy"
data-testid="location-permission-action"
@click="onLocationAction"
>
{{
isBusy
? $t("location_permission.checking")
: canRequestPermission
? $t("location_permission.use_current")
: $t("common.retry")
}}
</button>
</div>
</template>
<style scoped>
.location-permission {
margin: 1rem;
text-align: center;
}
</style>
+9
View File
@@ -4527,6 +4527,15 @@
"succeeded": "@.capitalize:{'words.generated.succesfuld'}"
}
},
"location_permission": {
"title": "Brug din placering",
"explanation": "Brug din placering til at finde den nærmeste Truck Wash-afdeling.",
"use_current": "Brug min placering",
"checking": "Kontrollerer placering...",
"denied": "Placering er slået fra for Truck Wash. Vælg en afdeling manuelt, eller giv adgang i iOS-indstillingerne.",
"unavailable": "Din placering er ikke tilgængelig. Du kan stadig vælge en afdeling manuelt.",
"timeout": "Det tog for lang tid at finde din placering. Prøv igen, eller vælg en afdeling manuelt."
},
"login_qr": {
"camera_error": "@.capitalize:{'words.generated.kamera'} @:{'words.generated.fejl'}",
"login_with_password_instead": "@.capitalize:{'words.generated.log'} @:{'words.generated.ind'} @:{'words.generated.med'} @:{'words.generated.adgangskode'} @:{'words.generated.i'} @:{'words.generated.stedet'}",
+9
View File
@@ -4637,6 +4637,15 @@
"succeeded": "@:{'templates.generated.compat.global.succeeded'}"
}
},
"location_permission": {
"title": "Standort verwenden",
"explanation": "Verwenden Sie Ihren Standort, um die nächste Truck Wash-Niederlassung zu finden.",
"use_current": "Meinen Standort verwenden",
"checking": "Standort wird geprüft...",
"denied": "Der Standortzugriff ist für Truck Wash deaktiviert. Wählen Sie eine Niederlassung manuell oder erlauben Sie den Zugriff in den iOS-Einstellungen.",
"unavailable": "Ihr Standort ist nicht verfügbar. Sie können weiterhin eine Niederlassung manuell auswählen.",
"timeout": "Die Standortbestimmung hat zu lange gedauert. Versuchen Sie es erneut oder wählen Sie eine Niederlassung manuell."
},
"login_qr": {
"camera_error": "Kamerafehler",
"login_with_password_instead": "Stattdessen @:{'words.generated.mit'} @:{'words.generated.passwort'} @:{'words.generated.anmelden'}",
+9
View File
@@ -4358,6 +4358,15 @@
"succeeded": "@.capitalize:{'words.generated.successful'}"
}
},
"location_permission": {
"title": "Use your location",
"explanation": "Use your location to find the nearest Truck Wash department.",
"use_current": "Use my location",
"checking": "Checking location...",
"denied": "Location is disabled for Truck Wash. Select a department manually or allow access in iOS Settings.",
"unavailable": "Your location is unavailable. You can still select a department manually.",
"timeout": "Finding your location took too long. Try again or select a department manually."
},
"login_qr": {
"camera_error": "@.capitalize:{'words.generated.camera'} @.capitalize:{'words.generated.error'}",
"login_with_password_instead": "@.capitalize:{'words.generated.login'} @:{'words.generated.with'} @:{'words.generated.password'} @:{'words.generated.instead'}",
+9
View File
@@ -3693,6 +3693,15 @@
"succeeded": "@:{'templates.generated.compat.invoicing.progress.succeeded'}"
}
},
"location_permission": {
"title": "@:{'templates.generated.compat.location_permission.title'}",
"explanation": "@:{'templates.generated.compat.location_permission.explanation'}",
"use_current": "@:{'templates.generated.compat.location_permission.use_current'}",
"checking": "@:{'templates.generated.compat.location_permission.checking'}",
"denied": "@:{'templates.generated.compat.location_permission.denied'}",
"unavailable": "@:{'templates.generated.compat.location_permission.unavailable'}",
"timeout": "@:{'templates.generated.compat.location_permission.timeout'}"
},
"login_qr": {
"camera_error": "@:{'templates.generated.compat.login_qr.camera_error'}",
"login_with_password_instead": "@:{'templates.generated.compat.login_qr.login_with_password_instead'}",
+9
View File
@@ -4640,6 +4640,15 @@
"succeeded": "@.capitalize:{'words.generated.vellykket'}"
}
},
"location_permission": {
"title": "Bruk posisjonen din",
"explanation": "Bruk posisjonen din til å finne nærmeste Truck Wash-avdeling.",
"use_current": "Bruk posisjonen min",
"checking": "Kontrollerer posisjon...",
"denied": "Posisjon er slått av for Truck Wash. Velg en avdeling manuelt, eller gi tilgang i iOS-innstillingene.",
"unavailable": "Posisjonen din er ikke tilgjengelig. Du kan fortsatt velge en avdeling manuelt.",
"timeout": "Det tok for lang tid å finne posisjonen din. Prøv igjen, eller velg en avdeling manuelt."
},
"login_qr": {
"camera_error": "Kamerafeil",
"login_with_password_instead": "@.capitalize:{'words.generated.logg'} @:{'words.generated.pa'} @:{'words.generated.med'} @:{'words.generated.passord'} @:{'words.generated.i'} @:{'words.generated.stedet'}",
+9
View File
@@ -4690,6 +4690,15 @@
"succeeded": "Lyckad"
}
},
"location_permission": {
"title": "Använd din plats",
"explanation": "Använd din plats för att hitta närmaste Truck Wash-avdelning.",
"use_current": "Använd min plats",
"checking": "Kontrollerar plats...",
"denied": "Platsåtkomst är avstängd för Truck Wash. Välj en avdelning manuellt eller tillåt åtkomst i iOS-inställningarna.",
"unavailable": "Din plats är inte tillgänglig. Du kan fortfarande välja en avdelning manuellt.",
"timeout": "Det tog för lång tid att hitta din plats. Försök igen eller välj en avdelning manuellt."
},
"login_qr": {
"camera_error": "Kamerafel",
"login_with_password_instead": "@.capitalize:{'words.generated.logga'} @:{'words.generated.in'} @:{'words.generated.med'} @:{'words.generated.losenord'} @:{'words.generated.i'} stället",
@@ -0,0 +1,13 @@
{
"compat": {
"location_permission": {
"title": "Brug din placering",
"explanation": "Brug din placering til at finde den nærmeste Truck Wash-afdeling.",
"use_current": "Brug min placering",
"checking": "Kontrollerer placering...",
"denied": "Placering er slået fra for Truck Wash. Vælg en afdeling manuelt, eller giv adgang i iOS-indstillingerne.",
"unavailable": "Din placering er ikke tilgængelig. Du kan stadig vælge en afdeling manuelt.",
"timeout": "Det tog for lang tid at finde din placering. Prøv igen, eller vælg en afdeling manuelt."
}
}
}
@@ -0,0 +1,13 @@
{
"compat": {
"location_permission": {
"title": "Standort verwenden",
"explanation": "Verwenden Sie Ihren Standort, um die nächste Truck Wash-Niederlassung zu finden.",
"use_current": "Meinen Standort verwenden",
"checking": "Standort wird geprüft...",
"denied": "Der Standortzugriff ist für Truck Wash deaktiviert. Wählen Sie eine Niederlassung manuell oder erlauben Sie den Zugriff in den iOS-Einstellungen.",
"unavailable": "Ihr Standort ist nicht verfügbar. Sie können weiterhin eine Niederlassung manuell auswählen.",
"timeout": "Die Standortbestimmung hat zu lange gedauert. Versuchen Sie es erneut oder wählen Sie eine Niederlassung manuell."
}
}
}
@@ -0,0 +1,13 @@
{
"compat": {
"location_permission": {
"title": "Use your location",
"explanation": "Use your location to find the nearest Truck Wash department.",
"use_current": "Use my location",
"checking": "Checking location...",
"denied": "Location is disabled for Truck Wash. Select a department manually or allow access in iOS Settings.",
"unavailable": "Your location is unavailable. You can still select a department manually.",
"timeout": "Finding your location took too long. Try again or select a department manually."
}
}
}
@@ -0,0 +1,11 @@
{
"location_permission": {
"title": "@:{'phrases.compat.location_permission.title'}",
"explanation": "@:{'phrases.compat.location_permission.explanation'}",
"use_current": "@:{'phrases.compat.location_permission.use_current'}",
"checking": "@:{'phrases.compat.location_permission.checking'}",
"denied": "@:{'phrases.compat.location_permission.denied'}",
"unavailable": "@:{'phrases.compat.location_permission.unavailable'}",
"timeout": "@:{'phrases.compat.location_permission.timeout'}"
}
}
@@ -0,0 +1,13 @@
{
"compat": {
"location_permission": {
"title": "Bruk posisjonen din",
"explanation": "Bruk posisjonen din til å finne nærmeste Truck Wash-avdeling.",
"use_current": "Bruk posisjonen min",
"checking": "Kontrollerer posisjon...",
"denied": "Posisjon er slått av for Truck Wash. Velg en avdeling manuelt, eller gi tilgang i iOS-innstillingene.",
"unavailable": "Posisjonen din er ikke tilgjengelig. Du kan fortsatt velge en avdeling manuelt.",
"timeout": "Det tok for lang tid å finne posisjonen din. Prøv igjen, eller velg en avdeling manuelt."
}
}
}
@@ -0,0 +1,13 @@
{
"compat": {
"location_permission": {
"title": "Använd din plats",
"explanation": "Använd din plats för att hitta närmaste Truck Wash-avdelning.",
"use_current": "Använd min plats",
"checking": "Kontrollerar plats...",
"denied": "Platsåtkomst är avstängd för Truck Wash. Välj en avdelning manuellt eller tillåt åtkomst i iOS-inställningarna.",
"unavailable": "Din plats är inte tillgänglig. Du kan fortfarande välja en avdelning manuellt.",
"timeout": "Det tog för lång tid att hitta din plats. Försök igen eller välj en avdelning manuellt."
}
}
}
+213
View File
@@ -0,0 +1,213 @@
import { Capacitor } from "@capacitor/core";
import { Geolocation, type PermissionState, type Position } from "@capacitor/geolocation";
export type DeviceLocationPermission = "granted" | "prompt" | "denied" | "unavailable";
export type DeviceLocationErrorCode =
| "permission-denied"
| "services-disabled"
| "position-unavailable"
| "timeout"
| "unknown";
export type LocationRequestOptions = {
promptIfNeeded?: boolean;
enableHighAccuracy?: boolean;
maximumAge?: number;
timeout?: number;
};
export type LocationResult = {
permission: DeviceLocationPermission;
coords: {
latitude: number;
longitude: number;
} | null;
timestamp: number | null;
errorCode: DeviceLocationErrorCode | null;
errorMessage: string | null;
};
const resultWithoutPosition = (
permission: DeviceLocationPermission,
errorCode: DeviceLocationErrorCode | null = null,
errorMessage: string | null = null
): LocationResult => ({
permission,
coords: null,
timestamp: null,
errorCode,
errorMessage,
});
const mapPermission = (permission: PermissionState | undefined): DeviceLocationPermission => {
if (permission === "granted") return "granted";
if (permission === "denied") return "denied";
return "prompt";
};
const mapErrorCode = (error: unknown): DeviceLocationErrorCode => {
const code =
typeof error === "object" && error !== null && "code" in error
? String((error as { code?: unknown }).code)
: "";
if (code === "1" || code === "OS-PLUG-GLOC-0003" || code === "PERMISSION_DENIED") {
return "permission-denied";
}
if (code === "2" || code === "OS-PLUG-GLOC-0007" || code === "POSITION_UNAVAILABLE") {
return code === "OS-PLUG-GLOC-0007" ? "services-disabled" : "position-unavailable";
}
if (code === "3" || code === "OS-PLUG-GLOC-0010" || code === "TIMEOUT") {
return "timeout";
}
return "unknown";
};
const errorMessage = (error: unknown): string =>
error instanceof Error
? error.message
: typeof error === "object" && error !== null && "message" in error
? String((error as { message?: unknown }).message ?? "")
: String(error ?? "");
const resultFromPosition = (position: Position): LocationResult => ({
permission: "granted",
coords: {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
},
timestamp: Number.isFinite(position.timestamp) ? position.timestamp : Date.now(),
errorCode: null,
errorMessage: null,
});
let nativePermissionRequest: Promise<DeviceLocationPermission> | null = null;
let positionRequest: Promise<LocationResult> | null = null;
const requestNativePermission = async (): Promise<DeviceLocationPermission> => {
nativePermissionRequest ??= Geolocation.requestPermissions({ permissions: ["location"] })
.then((status) => mapPermission(status.location))
.finally(() => {
nativePermissionRequest = null;
});
return nativePermissionRequest;
};
const nativeLocation = async (options: Required<LocationRequestOptions>): Promise<LocationResult> => {
let permission: DeviceLocationPermission;
try {
const status = await Geolocation.checkPermissions();
permission = mapPermission(status.location);
} catch (error) {
return resultWithoutPosition("unavailable", mapErrorCode(error), errorMessage(error));
}
if (permission === "prompt" && options.promptIfNeeded) {
try {
permission = await requestNativePermission();
} catch (error) {
const code = mapErrorCode(error);
return resultWithoutPosition(
code === "permission-denied" ? "denied" : "unavailable",
code,
errorMessage(error)
);
}
}
if (permission !== "granted") {
return resultWithoutPosition(
permission,
permission === "denied" ? "permission-denied" : null
);
}
try {
const position = await Geolocation.getCurrentPosition({
enableHighAccuracy: options.enableHighAccuracy,
maximumAge: options.maximumAge,
timeout: options.timeout,
});
return resultFromPosition(position);
} catch (error) {
return resultWithoutPosition("granted", mapErrorCode(error), errorMessage(error));
}
};
const webPermission = async (): Promise<DeviceLocationPermission> => {
if (typeof navigator === "undefined" || !navigator.geolocation) {
return "unavailable";
}
if (!navigator.permissions?.query) {
return "prompt";
}
try {
const status = await navigator.permissions.query({ name: "geolocation" });
return status.state === "granted" ? "granted" : status.state === "denied" ? "denied" : "prompt";
} catch {
return "prompt";
}
};
const webLocation = async (options: Required<LocationRequestOptions>): Promise<LocationResult> => {
const permission = await webPermission();
if (permission === "unavailable") {
return resultWithoutPosition("unavailable", "position-unavailable");
}
if (permission === "denied") {
return resultWithoutPosition("denied", "permission-denied");
}
if (permission === "prompt" && !options.promptIfNeeded) {
return resultWithoutPosition("prompt");
}
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(
(position) =>
resolve({
permission: "granted",
coords: {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
},
timestamp: Number.isFinite(position.timestamp) ? position.timestamp : Date.now(),
errorCode: null,
errorMessage: null,
}),
(error) => {
const code = mapErrorCode(error);
resolve(
resultWithoutPosition(
code === "permission-denied" ? "denied" : permission,
code,
error.message
)
);
},
{
enableHighAccuracy: options.enableHighAccuracy,
maximumAge: options.maximumAge,
timeout: options.timeout,
}
);
});
};
export const getCurrentDeviceLocation = (
requestOptions: LocationRequestOptions = {}
): Promise<LocationResult> => {
const options: Required<LocationRequestOptions> = {
promptIfNeeded: requestOptions.promptIfNeeded ?? false,
enableHighAccuracy: requestOptions.enableHighAccuracy ?? true,
maximumAge: requestOptions.maximumAge ?? 30_000,
timeout: requestOptions.timeout ?? 27_000,
};
positionRequest ??= (Capacitor.isNativePlatform() ? nativeLocation(options) : webLocation(options)).finally(
() => {
positionRequest = null;
}
);
return positionRequest;
};
+8
View File
@@ -252,6 +252,13 @@ const selectManualDepartment = (department) => {
redirectToDepartmentPos(department);
}
const onLocationPermissionStateChanged = (permission) => {
if (permission !== "granted" && !autoSelectDepartmentResolved.value) {
manualDepartmentSelectionAvailable.value = true;
clearManualDepartmentSelectionTimeout();
}
}
const startMobileDepartmentAutoSelect = () => {
if (autoSelectDepartmentResolved.value) {
return;
@@ -392,6 +399,7 @@ onUnmounted(() => {
<PosDepartmentStepMobile1Location
:maximum-age="autoSelectGeolocationMaximumAge"
:timeout="autoSelectGeolocationTimeout"
@permission-state-changed="onLocationPermissionStateChanged"
/>
<div
v-if="manualDepartmentSelectionAvailable && !autoSelectDepartmentResolved"
+1
View File
@@ -373,6 +373,7 @@ test.describe("User MyWashStart mocked flow", () => {
await primeMockSession(page, { token: "user-my-wash-no-geolocation-token", bootPath: "/user/wash/start" });
await expect(page.getByTestId("location-permission-action")).toBeVisible({ timeout: 15_000 });
await expect(page.getByTestId("self-serve-department-name")).toContainText("Roskilde", { timeout: 15_000 });
await fillRegistration(page);
await selectVehicleType(page, 2);
@@ -59,6 +59,7 @@ vi.mock("@/components/displays/department/pos/steps/mobile/elements/PosDepartmen
return {
default: defineComponent({
name: "MockPosDepartmentStepMobile1Location",
emits: ["permission-state-changed"],
props: {
enableHighAccuracy: {
type: Boolean,
@@ -320,6 +321,20 @@ describe("DefaultPage mobile department redirect", () => {
expect(mounted.find('[data-testid="redirect-manual-department-button"]').exists()).toBe(true);
});
it("shows manual department selection immediately when location permission needs a user action", async () => {
const mounted = mountDefaultPage();
await nextTick();
expect(mounted.find('[data-testid="redirect-manual-department-button"]').exists()).toBe(false);
mounted
.getComponent({ name: "MockPosDepartmentStepMobile1Location" })
.vm.$emit("permission-state-changed", "prompt");
await nextTick();
expect(mounted.find('[data-testid="redirect-manual-department-button"]').exists()).toBe(true);
});
it("lets the user manually select a department after GPS wait", async () => {
setAdminSession(["admin", "department_access_4", "department_access_6"]);
__departmentsRef.value = [
+150
View File
@@ -0,0 +1,150 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const capacitorMocks = vi.hoisted(() => ({
isNativePlatform: vi.fn(),
checkPermissions: vi.fn(),
requestPermissions: vi.fn(),
getCurrentPosition: vi.fn(),
}));
vi.mock("@capacitor/core", () => ({
Capacitor: {
isNativePlatform: capacitorMocks.isNativePlatform,
},
}));
vi.mock("@capacitor/geolocation", () => ({
Geolocation: {
checkPermissions: capacitorMocks.checkPermissions,
requestPermissions: capacitorMocks.requestPermissions,
getCurrentPosition: capacitorMocks.getCurrentPosition,
},
}));
const position = {
coords: {
latitude: 55.6761,
longitude: 12.5683,
},
timestamp: 1_721_234_567_890,
};
const loadService = async () => {
vi.resetModules();
return import("@/services/deviceLocation");
};
describe("device location permission flow", () => {
beforeEach(() => {
capacitorMocks.isNativePlatform.mockReset();
capacitorMocks.checkPermissions.mockReset();
capacitorMocks.requestPermissions.mockReset();
capacitorMocks.getCurrentPosition.mockReset();
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("does not show the native permission prompt during a silent startup check", async () => {
capacitorMocks.isNativePlatform.mockReturnValue(true);
capacitorMocks.checkPermissions.mockResolvedValue({ location: "prompt" });
const { getCurrentDeviceLocation } = await loadService();
const result = await getCurrentDeviceLocation({ promptIfNeeded: false });
expect(result.permission).toBe("prompt");
expect(result.coords).toBeNull();
expect(capacitorMocks.requestPermissions).not.toHaveBeenCalled();
expect(capacitorMocks.getCurrentPosition).not.toHaveBeenCalled();
});
it("requests native permission only after the explicit location action", async () => {
capacitorMocks.isNativePlatform.mockReturnValue(true);
capacitorMocks.checkPermissions.mockResolvedValue({ location: "prompt" });
capacitorMocks.requestPermissions.mockResolvedValue({ location: "granted" });
capacitorMocks.getCurrentPosition.mockResolvedValue(position);
const { getCurrentDeviceLocation } = await loadService();
const result = await getCurrentDeviceLocation({ promptIfNeeded: true });
expect(capacitorMocks.requestPermissions).toHaveBeenCalledTimes(1);
expect(capacitorMocks.getCurrentPosition).toHaveBeenCalledTimes(1);
expect(result).toMatchObject({
permission: "granted",
coords: position.coords,
timestamp: position.timestamp,
errorCode: null,
});
});
it("silently reuses a persistent native grant on later app starts", async () => {
capacitorMocks.isNativePlatform.mockReturnValue(true);
capacitorMocks.checkPermissions.mockResolvedValue({ location: "granted" });
capacitorMocks.getCurrentPosition.mockResolvedValue(position);
const { getCurrentDeviceLocation } = await loadService();
const result = await getCurrentDeviceLocation();
expect(capacitorMocks.requestPermissions).not.toHaveBeenCalled();
expect(capacitorMocks.getCurrentPosition).toHaveBeenCalledTimes(1);
expect(result.coords).toEqual(position.coords);
});
it("does not reprompt after native permission has been denied", async () => {
capacitorMocks.isNativePlatform.mockReturnValue(true);
capacitorMocks.checkPermissions.mockResolvedValue({ location: "denied" });
const { getCurrentDeviceLocation } = await loadService();
const result = await getCurrentDeviceLocation({ promptIfNeeded: true });
expect(result).toMatchObject({
permission: "denied",
errorCode: "permission-denied",
});
expect(capacitorMocks.requestPermissions).not.toHaveBeenCalled();
expect(capacitorMocks.getCurrentPosition).not.toHaveBeenCalled();
});
it("does not call browser geolocation while web permission is still prompt", async () => {
capacitorMocks.isNativePlatform.mockReturnValue(false);
const getCurrentPosition = vi.fn();
vi.stubGlobal("navigator", {
permissions: {
query: vi.fn().mockResolvedValue({ state: "prompt" }),
},
geolocation: {
getCurrentPosition,
},
});
const { getCurrentDeviceLocation } = await loadService();
const result = await getCurrentDeviceLocation();
expect(result.permission).toBe("prompt");
expect(getCurrentPosition).not.toHaveBeenCalled();
});
it("uses one browser position request when permission is already granted", async () => {
capacitorMocks.isNativePlatform.mockReturnValue(false);
const watchPosition = vi.fn();
const getCurrentPosition = vi.fn((success) => success(position));
vi.stubGlobal("navigator", {
permissions: {
query: vi.fn().mockResolvedValue({ state: "granted" }),
},
geolocation: {
getCurrentPosition,
watchPosition,
},
});
const { getCurrentDeviceLocation } = await loadService();
const result = await getCurrentDeviceLocation();
expect(result.coords).toEqual(position.coords);
expect(getCurrentPosition).toHaveBeenCalledTimes(1);
expect(watchPosition).not.toHaveBeenCalled();
});
});
@@ -8,6 +8,9 @@ const mainStylesSource = readFileSync(join(root, "src/assets/main.css"), "utf8")
const mobileHeaderSource = readFileSync(join(root, "src/components/viewport/page/headers/MobileHeader.vue"), "utf8");
const desktopHeaderSource = readFileSync(join(root, "src/components/viewport/page/headers/DesktopHeader.vue"), "utf8");
const tabletHeaderSource = readFileSync(join(root, "src/components/viewport/page/headers/TabletHeader.vue"), "utf8");
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
const capacitorConfigSource = readFileSync(join(root, "capacitor.config.ts"), "utf8");
const iosInfoPlistSource = readFileSync(join(root, "ios/App/App/Info.plist"), "utf8");
describe("mobile header iOS safe-area contract", () => {
it("opts into safe-area viewport geometry", () => {
@@ -25,4 +28,12 @@ describe("mobile header iOS safe-area contract", () => {
expect(desktopHeaderSource).not.toContain("safe-area-inset-top");
expect(tabletHeaderSource).not.toContain("safe-area-inset-top");
});
it("keeps the native iOS status bar outside the Capacitor web view", () => {
expect(packageJson.dependencies["@capacitor/status-bar"]).toBe("^8.0.3");
expect(capacitorConfigSource).toMatch(
/StatusBar:\s*\{[\s\S]*overlaysWebView:\s*false,[\s\S]*style:\s*"LIGHT",[\s\S]*backgroundColor:\s*"#FFFFFFFF"/
);
expect(iosInfoPlistSource).toMatch(/<key>UIViewControllerBasedStatusBarAppearance<\/key>\s*<true\/>/);
});
});
@@ -0,0 +1,90 @@
// @vitest-environment jsdom
import { defineComponent, h, nextTick } from "vue";
import { mount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
const locationMocks = vi.hoisted(() => ({
getCurrentDeviceLocation: vi.fn(),
set: vi.fn(),
}));
vi.mock("@/services/deviceLocation", () => ({
getCurrentDeviceLocation: locationMocks.getCurrentDeviceLocation,
}));
vi.mock("@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue", () => ({
locations: {
normalizeCoordinatePair: (coords) => coords,
set: locationMocks.set,
},
default: defineComponent({
render: () => h("div"),
}),
}));
import PosDepartmentStepMobile1Location from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1Location.vue";
const mountLocation = () =>
mount(PosDepartmentStepMobile1Location, {
global: {
mocks: {
$t: (key) => key,
},
},
});
describe("mobile location permission control", () => {
beforeEach(() => {
locationMocks.getCurrentDeviceLocation.mockReset();
locationMocks.set.mockReset();
});
it("checks silently on mount and prompts only from the location button", async () => {
locationMocks.getCurrentDeviceLocation
.mockResolvedValueOnce({
permission: "prompt",
coords: null,
timestamp: null,
errorCode: null,
errorMessage: null,
})
.mockResolvedValueOnce({
permission: "granted",
coords: {
latitude: 55.6761,
longitude: 12.5683,
},
timestamp: 1_721_234_567_890,
errorCode: null,
errorMessage: null,
});
const wrapper = mountLocation();
await nextTick();
await nextTick();
expect(locationMocks.getCurrentDeviceLocation).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ promptIfNeeded: false })
);
expect(wrapper.get('[data-testid="location-permission-control"]').exists()).toBe(true);
await wrapper.get('[data-testid="location-permission-action"]').trigger("click");
await nextTick();
await nextTick();
expect(locationMocks.getCurrentDeviceLocation).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ promptIfNeeded: true })
);
expect(locationMocks.set).toHaveBeenCalledWith(
expect.objectContaining({
coords: {
latitude: 55.6761,
longitude: 12.5683,
},
})
);
expect(wrapper.emitted("location-updated")).toEqual([[{ latitude: 55.6761, longitude: 12.5683 }]]);
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB