ink-framework-wallet 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +24 -2
- package/ink-native.json +1 -0
- package/package.json +24 -4
- package/src/index.tsx +115 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vandam Dinh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Wallet pass previews
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@ink/wallet` previews single `.pkpass` attachments on Android. `wallet.preview(source, signal)` accepts an HTTP(S) URL or an `ink-file://` managed file. Native code caches static previews by source, reusing the saved metadata and artwork before downloading or reading the ZIP. Missing artwork is rebuilt from the cached archive. Source keys are hashed, with up to eight source aliases per archive and 32 cached archives. `PassPreview` displays its fields and supported barcode using Ink components.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
const pass = await wallet.preview(attachment.src, signal);
|
|
7
|
+
<PassPreview pass={pass} />
|
|
8
|
+
if (await wallet.canOpen("com.vandamd.passes")) {
|
|
9
|
+
await wallet.open(pass, "com.vandamd.passes");
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Receiving apps implement an exported `com.vandam.ink.PassImportActivity`. The sender must declare the receiver package in Android `<queries>` for `canOpen`. The hand-off is an explicit `ACTION_VIEW` containing a FileProvider content URI, PKPass MIME type and temporary read permission. Copy the file while that permission is active. Passes implements this contract and asks the user before saving.
|
|
14
|
+
|
|
15
|
+
Conversation rows use `filePreview: { title, label, artwork? }` and `onFilePress(message)` to reuse the native link-card layout without representing an attachment as a web link. Artwork appears in a 56 × 56 square beside the labels, preserving its aspect ratio. Beeper loads pass artwork in the background and leaves the text card usable if no artwork is available.
|
|
16
|
+
|
|
17
|
+
Artwork: logo, strip, thumbnail, background, footer and icon PNGs, preferring @3x then @2x. Artwork is displayed as images alongside the fields, with background used when no strip is present. `wallet.retain(pass)` makes artwork durable before saving its returned metadata; `wallet.release(pass)` removes saved artwork when deleting a pass. Preview eviction does not remove retained artwork.
|
|
18
|
+
|
|
19
|
+
Layout: pass field groups drive paired primary fields (with a transit arrow for boarding passes), paired details and a centred barcode with its `altText` caption. The decorative logo/title row is hidden to keep the ticket compact. Header fields remain visible as ordinary detail rows; an unlabelled boarding-pass header that repeats the displayed route is omitted. Back fields are available through Passes’ action menu’s Info page; Beeper hides them. `wallet.details(pass)` returns labelled text parts with link destinations, parsing HTML anchors and recognising plain web and email links. Render linked parts with `Text href` for underlined, tappable links. Older saved passes without group metadata retain all fields in paired rows; reimport to restore the original grouping.
|
|
20
|
+
|
|
21
|
+
Supported: classic boarding pass, coupon, event ticket, generic and store card fields; QR, PDF417, Aztec and Code128 barcodes. UTF-8 is supported, along with ASCII data in recognised legacy encodings. Unsupported barcode formats or non-ASCII legacy encodings are omitted rather than silently changing the encoded data. Passes can still save the fields.
|
|
22
|
+
|
|
23
|
+
This is a static preview, not a full Apple Wallet implementation: no signature verification, exact Wallet layout reproduction, localisation, live updates, NFC or multi-pass archives. Date and time styles use the device locale, respecting `ignoresTimeZone`; invalid dates retain their source text. Numbers retain their source values. Cached previews are regenerated when the parser version changes. See [Apple's pass format](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/Creating.html).
|
|
24
|
+
|
|
25
|
+
Limits: 10 MB per archive, 256 ZIP entries and 256 KB of `pass.json`, and 1 KB of barcode payload. Only the six recognised artwork filenames are read, with generated managed-file names. Each image is bounded to 2 MB, 4096 pixels per side and 8 megapixels. Only the 32 most recent archives remain in the preview cache; a missing cached archive requires reopening its preview. Passes stores the decoded fields/barcode separately, so a saved pass remains available offline.
|
package/ink-native.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "version": 1, "modules": { "src/index.tsx": ["files", "network", "file-images", "image"] } }
|
package/package.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ink-framework-wallet",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": "./src/index.tsx"
|
|
6
|
+
},
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"valibot": "1.5.0"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"ink": "npm:ink-framework@0.1.0-alpha.1",
|
|
12
|
+
"react": "19.2.8",
|
|
13
|
+
"@ink/barcode": "npm:ink-framework-barcode@0.1.0-alpha.1"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/No-Scrolling/ink.git",
|
|
19
|
+
"directory": "packages/wallet"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"*.json",
|
|
24
|
+
"*.ts"
|
|
25
|
+
]
|
|
26
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import { Icon, Image, Stack, Text } from "ink";
|
|
3
|
+
import { arrowForward } from "ink/icons";
|
|
4
|
+
import { callNative } from "ink/native";
|
|
5
|
+
import { Barcode, supportedFormats } from "@ink/barcode";
|
|
6
|
+
|
|
7
|
+
const artworkSchema = v.object({ src: v.string(), width: v.number(), height: v.number() });
|
|
8
|
+
export const walletPassSchema = v.object({
|
|
9
|
+
id: v.string(),
|
|
10
|
+
serialNumber: v.string(),
|
|
11
|
+
passTypeIdentifier: v.string(),
|
|
12
|
+
title: v.string(),
|
|
13
|
+
organisation: v.string(),
|
|
14
|
+
description: v.string(),
|
|
15
|
+
artwork: v.optional(v.object({
|
|
16
|
+
logo: v.optional(artworkSchema), strip: v.optional(artworkSchema), thumbnail: v.optional(artworkSchema),
|
|
17
|
+
background: v.optional(artworkSchema), footer: v.optional(artworkSchema), icon: v.optional(artworkSchema),
|
|
18
|
+
})),
|
|
19
|
+
style: v.optional(v.string()),
|
|
20
|
+
fields: v.array(v.object({ label: v.string(), value: v.string(), group: v.optional(v.string()), displayValue: v.optional(v.string()) })),
|
|
21
|
+
barcode: v.optional(v.object({ format: v.picklist(supportedFormats), value: v.string(), altText: v.optional(v.string()) })),
|
|
22
|
+
});
|
|
23
|
+
const detailFieldsSchema = v.array(v.object({
|
|
24
|
+
label: v.string(),
|
|
25
|
+
parts: v.array(v.object({ text: v.string(), url: v.optional(v.string()) })),
|
|
26
|
+
}));
|
|
27
|
+
export type PassDetailField = v.InferOutput<typeof detailFieldsSchema>[number];
|
|
28
|
+
export type WalletPass = v.InferOutput<typeof walletPassSchema>;
|
|
29
|
+
|
|
30
|
+
export function isWalletPass(file: { name?: string; mimeType?: string; src?: string }) {
|
|
31
|
+
return file.mimeType?.split(";")[0].trim().toLowerCase() === "application/vnd.apple.pkpass"
|
|
32
|
+
|| /\.pkpass(?:[?#]|$)/i.test(file.name || "")
|
|
33
|
+
|| /\.pkpass(?:[?#]|$)/i.test(file.src || "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const wallet = {
|
|
37
|
+
async preview(source: string, signal?: AbortSignal): Promise<WalletPass> {
|
|
38
|
+
return v.parse(walletPassSchema, JSON.parse(await callNative("files", "pass-preview", { source }, { signal })));
|
|
39
|
+
},
|
|
40
|
+
async details(pass: WalletPass): Promise<PassDetailField[]> {
|
|
41
|
+
return v.parse(detailFieldsSchema, JSON.parse(await callNative("files", "pass-details", { pass })));
|
|
42
|
+
},
|
|
43
|
+
async retain(pass: WalletPass): Promise<WalletPass> {
|
|
44
|
+
return v.parse(walletPassSchema, JSON.parse(await callNative("files", "pass-retain", { pass })));
|
|
45
|
+
},
|
|
46
|
+
async release(pass: WalletPass): Promise<void> {
|
|
47
|
+
for (const image of Object.values(pass.artwork || {})) {
|
|
48
|
+
if (image?.src.startsWith(`ink-file://${pass.id}-s-`))
|
|
49
|
+
await callNative("files", "remove", { id: image.src.slice("ink-file://".length) });
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async canOpen(packageName: string): Promise<boolean> {
|
|
53
|
+
return await callNative("files", "pass-can-open", { packageName }) === "true";
|
|
54
|
+
},
|
|
55
|
+
async open(pass: WalletPass, packageName: string): Promise<void> {
|
|
56
|
+
await callNative("files", "pass-open", { id: pass.id, packageName });
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function Artwork({ image, width = 300, fillWidth = false }: { image?: v.InferOutput<typeof artworkSchema>; width?: number; fillWidth?: boolean }) {
|
|
61
|
+
if (!image) return null;
|
|
62
|
+
if (fillWidth) return <Image src={image.src} width={image.width} height={image.height} fillWidth fit="contain" />;
|
|
63
|
+
width = Math.min(width, image.width);
|
|
64
|
+
const height = Math.min(200, width * image.height / image.width);
|
|
65
|
+
return <Image src={image.src} width={width} height={height} fit="contain" />;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type PassField = WalletPass["fields"][number];
|
|
69
|
+
function Field({ field, primary = false, paired = false, end = false }: { field: PassField; primary?: boolean; paired?: boolean; end?: boolean }) {
|
|
70
|
+
return <Stack gap={0} align={end ? "end" : "start"}>
|
|
71
|
+
{field.label && <Text size={11} width={primary && paired ? 140 : undefined} align={end ? "end" : "start"}>{field.label.toUpperCase()}</Text>}
|
|
72
|
+
<Text size={primary ? 36 : 16} width={primary && paired ? 140 : undefined} align={end ? "end" : "start"}>{field.displayValue ?? field.value}</Text>
|
|
73
|
+
</Stack>;
|
|
74
|
+
}
|
|
75
|
+
function FieldRows({ fields, primary = false, transit = false }: { fields: PassField[]; primary?: boolean; transit?: boolean }) {
|
|
76
|
+
const rows = [];
|
|
77
|
+
for (let index = 0; index < fields.length; index += 2) {
|
|
78
|
+
rows.push(<Stack key={index} axis="horizontal" justify="space-between" align="center" gap={6}>
|
|
79
|
+
<Field field={fields[index]} primary={primary} paired={!!fields[index + 1]} />
|
|
80
|
+
{transit && index === 0 && fields[index + 1] && <Icon name={arrowForward} size={24} />}
|
|
81
|
+
{fields[index + 1] && <Field field={fields[index + 1]} primary={primary} paired end />}
|
|
82
|
+
</Stack>);
|
|
83
|
+
}
|
|
84
|
+
return <Stack gap={14} align="stretch">{rows}</Stack>;
|
|
85
|
+
}
|
|
86
|
+
function routeText(value: string) {
|
|
87
|
+
return value.replace(/\s+/g, "").replace(/[–—→]/g, "-").toUpperCase();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function PassPreview({ pass }: { pass: WalletPass }) {
|
|
91
|
+
const artwork = pass.artwork;
|
|
92
|
+
const fields = pass.fields.filter(field => field.label.trim() || field.value.trim());
|
|
93
|
+
const primary = fields.filter(field => field.group === "primaryFields");
|
|
94
|
+
const secondary = fields.filter(field => field.group === "secondaryFields" || !field.group);
|
|
95
|
+
const route = pass.style === "boardingPass" && primary.length === 2
|
|
96
|
+
? routeText(`${primary[0].value}-${primary[1].value}`) : undefined;
|
|
97
|
+
const headers = fields.filter(field => field.group === "headerFields"
|
|
98
|
+
&& !(route && !field.label.trim() && routeText(field.value.replace(/^[^:]*:/, "")) === route));
|
|
99
|
+
const auxiliary = fields.filter(field => field.group === "auxiliaryFields");
|
|
100
|
+
return <Stack gap={14} align="stretch" justify="space-between">
|
|
101
|
+
<Stack gap={14} align="stretch">
|
|
102
|
+
<Artwork image={artwork?.strip || artwork?.background} fillWidth />
|
|
103
|
+
<Artwork image={artwork?.thumbnail} width={80} />
|
|
104
|
+
{primary.length > 0 && <FieldRows fields={primary} primary transit={pass.style === "boardingPass"} />}
|
|
105
|
+
{(pass.style === "boardingPass" ? [auxiliary, secondary] : [secondary, auxiliary]).map((fields, index) =>
|
|
106
|
+
fields.length > 0 && <FieldRows key={index} fields={fields} />)}
|
|
107
|
+
{headers.length > 0 && <FieldRows fields={headers} />}
|
|
108
|
+
<Artwork image={artwork?.footer} />
|
|
109
|
+
</Stack>
|
|
110
|
+
{pass.barcode ? <Stack align="center" gap={5}>
|
|
111
|
+
<Barcode format={pass.barcode.format} value={pass.barcode.value} size={200} />
|
|
112
|
+
{pass.barcode.altText && <Text size={14} align="center">{pass.barcode.altText}</Text>}
|
|
113
|
+
</Stack> : <Text>This pass has no supported barcode.</Text>}
|
|
114
|
+
</Stack>;
|
|
115
|
+
}
|