diff --git a/astro.config.ts b/astro.config.ts index 5e55e256..d4693bcf 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -9,6 +9,7 @@ import starlightLlmsTxt from "starlight-llms-txt"; import { loadEnv } from "vite"; import { readFile } from "node:fs/promises"; import satteriExternalLinks from "./src/plugins/satteri/external-links"; +import satteriCallouts from "./src/plugins/satteri/callouts"; import { defineConfig } from "astro/config"; import type { HeadUserConfig } from "node_modules/@astrojs/starlight/schemas/head"; @@ -194,9 +195,11 @@ export default defineConfig({ markdown: { processor: satteri({ features: { + directive: true, gfm: true, smartPunctuation: true, }, + mdastPlugins: [satteriCallouts], hastPlugins: [satteriExternalLinks], }), }, @@ -334,6 +337,7 @@ export default defineConfig({ "./src/styles/theme-light.css", "./src/styles/starlight-vars.css", "./src/styles/utilities.css", + "./src/styles/callout.css", ], pagination: false, lastUpdated: true, diff --git a/ec.config.mjs b/ec.config.mjs index 8d7c9805..d381f41f 100644 --- a/ec.config.mjs +++ b/ec.config.mjs @@ -4,7 +4,7 @@ import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers"; export default defineEcConfig({ plugins: [pluginCollapsibleSections(), pluginLineNumbers()], - themes: ["github-dark-dimmed", "github-light-default"], + themes: ["github-dark-default", "github-light-default"], styleOverrides: { codeFontSize: "0.8rem", borderColor: "var(--cui-border-subtle)", diff --git a/src/components/ApiDocs/Operation.astro b/src/components/ApiDocs/Operation.astro index d5d76b0f..0226d9df 100644 --- a/src/components/ApiDocs/Operation.astro +++ b/src/components/ApiDocs/Operation.astro @@ -2,10 +2,13 @@ import { Badge, Tabs, TabItem } from "@astrojs/starlight/components"; import Callout from "@components/content/Callout"; import Markdown from "@components/Markdown.astro"; +import MarkdownContent from "@components/MarkdownContent.astro"; import { formatSlug } from "@lib/helpers"; import { resolveReference } from "@lib/openapi"; +import { getOperationDescriptionId } from "@lib/openapiContent"; import { Headline, Body, Anchor } from "@sumup-oss/circuit-ui"; import type { HTMLAttributes } from "astro/types"; +import { getEntry, render } from "astro:content"; import type { OpenAPIV3_1 } from "openapi-types"; import type { OperationObject, @@ -26,6 +29,24 @@ interface Props extends HTMLAttributes<"section"> { } const { operation, coreObjects = [], ...props } = Astro.props; +// OpenAPI descriptions are content entries so they use the same configured +// Markdown pipeline as authored docs instead of a component-local parser. +const descriptionEntry = operation.operationId + ? await getEntry( + "apiDescriptions", + getOperationDescriptionId(operation.operationId), + ) + : undefined; + +if (operation.description && !descriptionEntry) { + throw new Error( + `Missing Markdown content entry for operation ${operation.operationId ?? "without an operationId"}`, + ); +} + +const Description = descriptionEntry + ? (await render(descriptionEntry)).Content + : undefined; const isParameterObject = ( object: OpenAPIV3_1.ParameterObject | OpenAPIV3_1.ReferenceObject, @@ -260,10 +281,10 @@ const formattedPermissions = (operation["x-permissions"] || []) ) } { - operation.description && ( -
- {operation.description} -
+ operation.description && Description && ( + + + ) } { diff --git a/src/components/ApiDocs/TagSection.astro b/src/components/ApiDocs/TagSection.astro index c1b56988..6250a570 100644 --- a/src/components/ApiDocs/TagSection.astro +++ b/src/components/ApiDocs/TagSection.astro @@ -1,8 +1,11 @@ --- import Callout from "@components/content/Callout"; import Markdown from "@components/Markdown.astro"; +import MarkdownContent from "@components/MarkdownContent.astro"; import { formatSlug } from "@lib/helpers"; +import { getTagDescriptionId } from "@lib/openapiContent"; import type { HTMLAttributes } from "astro/types"; +import { getEntry, render } from "astro:content"; import type { OperationObject, TagObject } from "src/types/openapi"; import EndpointList from "./EndpointList"; import LRGrid from "./LRGrid.astro"; @@ -28,6 +31,20 @@ const { } = Astro.props; const slug = formatSlug(name); +// Tag descriptions are content entries so they use the configured Satteri +// pipeline, matching operation descriptions and authored documentation. +const descriptionEntry = await getEntry( + "apiDescriptions", + getTagDescriptionId(name), +); + +if (description && !descriptionEntry) { + throw new Error(`Missing Markdown content entry for tag ${name}`); +} + +const Description = descriptionEntry + ? (await render(descriptionEntry)).Content + : undefined; const [targetTag, targetOp] = (target || "").split("/"); const tagIsTarget = !targetOp && targetTag === slug; const objectPagefindAttributes = tagIsTarget @@ -80,7 +97,13 @@ const coreObjectSections = coreObjects.map((schema, index) => { ) } - {description} + { + description && Description && ( + + + + ) + } { operations.length > 0 && ( diff --git a/src/components/ApiDocs/TopSections.astro b/src/components/ApiDocs/TopSections.astro index 391a9030..dabe474d 100644 --- a/src/components/ApiDocs/TopSections.astro +++ b/src/components/ApiDocs/TopSections.astro @@ -1,4 +1,5 @@ --- +import MarkdownContent from "@components/MarkdownContent.astro"; import { CodeBlock, MultiCode } from "@components/Code"; import Markdown from "@components/Markdown.astro"; import type { ApiTopSection } from "@lib/openapi/routes"; @@ -72,14 +73,14 @@ const sectionAttrs = (id: ApiTopSection) =>
-
+ SDKs

The SumUp SDKs reduce the amount of work required to use our REST - APIs. SumUp maintains SDKs for PHP, JavaScript, Python, Java, Go, Rust, - and .NET. + APIs. SumUp maintains SDKs for PHP, JavaScript, Python, Java, Go, + Rust, and .NET.

-
+
@@ -167,7 +168,7 @@ uv add sumup`,
-
+ Authentication @@ -185,7 +186,7 @@ uv add sumup`, All API requests must be made over HTTPS and authenticated. Calls made over plain HTTP or calls without authentication will fail.

-
+
-
+ Errors

Newer APIs use{" "} @@ -381,7 +382,7 @@ let client = Client::default().with_authorization("sup_sk_MvxmLOl0...");`,

) } -
+
+ -
+ diff --git a/src/components/MarkdownContent.astro b/src/components/MarkdownContent.astro new file mode 100644 index 00000000..5d474380 --- /dev/null +++ b/src/components/MarkdownContent.astro @@ -0,0 +1,12 @@ +--- +import type { HTMLAttributes } from "astro/types"; +import "@astrojs/starlight/style/markdown.css"; + +type Props = HTMLAttributes<"div">; + +const { class: className, ...attrs } = Astro.props; +--- + +
+ +
diff --git a/src/components/content/Callout.module.css b/src/components/content/Callout.module.css deleted file mode 100644 index ad366479..00000000 --- a/src/components/content/Callout.module.css +++ /dev/null @@ -1,20 +0,0 @@ -.callout { - max-width: 100%; -} - -.callout :global(.cui-callout-content-kcln) { - min-width: 0; - max-width: 100%; -} - -.callout :global(.expressive-code), -.callout :global(.expressive-code .frame), -.callout :global(.expressive-code .frame pre), -.callout :global(pre) { - max-width: 100%; -} - -.callout :global(.expressive-code .frame pre), -.callout :global(pre) { - overflow-x: auto; -} diff --git a/src/components/content/Callout.tsx b/src/components/content/Callout.tsx index 94df6807..89fe1f20 100644 --- a/src/components/content/Callout.tsx +++ b/src/components/content/Callout.tsx @@ -1,7 +1,3 @@ -import { - Callout as CircuitCallout, - type CalloutColor, -} from "@sumup-oss/circuit-ui"; import { Confirm, Info, @@ -9,12 +5,10 @@ import { Sparkles, type IconComponentType, } from "@sumup-oss/icons"; -import type { ComponentProps, ReactNode } from "react"; -import styles from "./Callout.module.css"; - -type CalloutType = "note" | "tip" | "caution" | "success" | "promo"; +import type { HTMLAttributes, ReactNode } from "react"; +import type { CalloutType } from "./calloutTypes"; -type Props = Omit, "body" | "color"> & { +type Props = HTMLAttributes & { children: ReactNode; type?: CalloutType; }; @@ -22,28 +16,36 @@ type Props = Omit, "body" | "color"> & { const calloutConfig: Record< CalloutType, { - color: CalloutColor; icon: IconComponentType<"24">; iconLabel: string; } > = { - note: { color: "neutral", icon: Info, iconLabel: "Note" }, - tip: { color: "promo", icon: Sparkles, iconLabel: "Tip" }, - caution: { color: "alert", icon: Notify, iconLabel: "Caution" }, - success: { color: "confirm", icon: Confirm, iconLabel: "Success" }, - promo: { color: "promo", icon: Sparkles, iconLabel: "Promo" }, + note: { icon: Info, iconLabel: "Note" }, + tip: { icon: Sparkles, iconLabel: "Tip" }, + caution: { icon: Notify, iconLabel: "Caution" }, + success: { icon: Confirm, iconLabel: "Success" }, + promo: { icon: Sparkles, iconLabel: "Promo" }, }; -export default function Callout({ children, type = "note", ...props }: Props) { +export default function Callout({ + children, + className, + type = "note", + ...props +}: Props) { const config = calloutConfig[type]; - const className = [styles.callout, props.className].filter(Boolean).join(" "); + const classes = ["sumup-callout", `sumup-callout--${type}`, className] + .filter(Boolean) + .join(" "); + const Icon = config.icon; return ( - +
+
+
+ {config.iconLabel} +
{children}
+
); } diff --git a/src/components/content/calloutTypes.ts b/src/components/content/calloutTypes.ts new file mode 100644 index 00000000..7ee07dfd --- /dev/null +++ b/src/components/content/calloutTypes.ts @@ -0,0 +1,12 @@ +export const CALLOUT_TYPES = [ + "note", + "tip", + "caution", + "success", + "promo", +] as const; + +export type CalloutType = (typeof CALLOUT_TYPES)[number]; + +export const isCalloutType = (value: string): value is CalloutType => + CALLOUT_TYPES.includes(value as CalloutType); diff --git a/src/content.config.ts b/src/content.config.ts index c4dff19c..c626a5b6 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -2,6 +2,7 @@ import { docsLoader } from "@astrojs/starlight/loaders"; import { docsSchema } from "@astrojs/starlight/schema"; import { glob } from "astro/loaders"; import { defineCollection, z } from "astro:content"; +import { openapiDescriptionsLoader } from "./loaders/openapiDescriptions"; const help = defineCollection({ loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/help" }), @@ -19,6 +20,22 @@ const changelog = defineCollection({ }), }); +const apiDescriptions = defineCollection({ + loader: openapiDescriptionsLoader(), + schema: z.discriminatedUnion("kind", [ + z.object({ + kind: z.literal("tag"), + name: z.string(), + }), + z.object({ + kind: z.literal("operation"), + name: z.string(), + method: z.string(), + path: z.string(), + }), + ]), +}); + export const collections = { docs: defineCollection({ schema: docsSchema({ @@ -38,4 +55,5 @@ export const collections = { }), help, changelog, + apiDescriptions, }; diff --git a/src/content/changelog/android-tap-to-pay-1.1.6.md b/src/content/changelog/android-tap-to-pay-1.1.6.md index 89a936cf..264dfe0f 100644 --- a/src/content/changelog/android-tap-to-pay-1.1.6.md +++ b/src/content/changelog/android-tap-to-pay-1.1.6.md @@ -4,25 +4,25 @@ tags: ["Android Tap-to-pay SDK", "Android", "SDK"] publishedDate: 2026-08-25 --- -### New Features - -- Redesigned the payment UI (success, error, warning, loading, and tap-card screens), including updated animations and PIN-pad styling. -- When a presented card cannot be used, the SDK now prompts the cardholder to try another card instead of failing the payment immediately. -- After repeated unsuccessful “try another card” attempts, the payment fails with `PaymentException.CardErrorNotAccepted` (code `1029`). - -### Fixes - -- Fixed an issue that could send the charge request twice after a network interruption. -- Fixed an issue where a transaction that had already failed on the backend might not notify the integrator with `TransactionFailed`. -- Fixed a PIN-pad issue on some devices where digit 0 was not visible. - -### Integration Notes - -- The **Send receipt** button has been removed from the SDK success screen. Receipts must be handled by the host app if required. -- `PaymentFlowClosedSuccessfully.shouldDisplayReceipt` is deprecated, always emitted as `false`, and will be removed in **1.1.7**. Existing two-argument constructors still compile. -- `skipSuccessScreen` is unchanged. Hosts that already show their own success UI are unaffected. -- If you use an exhaustive `when` on `PaymentException`, add a branch for `PaymentException.CardErrorNotAccepted`. - -### Important — dependency and toolchain - +### New Features + +- Redesigned the payment UI (success, error, warning, loading, and tap-card screens), including updated animations and PIN-pad styling. +- When a presented card cannot be used, the SDK now prompts the cardholder to try another card instead of failing the payment immediately. +- After repeated unsuccessful “try another card” attempts, the payment fails with `PaymentException.CardErrorNotAccepted` (code `1029`). + +### Fixes + +- Fixed an issue that could send the charge request twice after a network interruption. +- Fixed an issue where a transaction that had already failed on the backend might not notify the integrator with `TransactionFailed`. +- Fixed a PIN-pad issue on some devices where digit 0 was not visible. + +### Integration Notes + +- The **Send receipt** button has been removed from the SDK success screen. Receipts must be handled by the host app if required. +- `PaymentFlowClosedSuccessfully.shouldDisplayReceipt` is deprecated, always emitted as `false`, and will be removed in **1.1.7**. Existing two-argument constructors still compile. +- `skipSuccessScreen` is unchanged. Hosts that already show their own success UI are unaffected. +- If you use an exhaustive `when` on `PaymentException`, add a branch for `PaymentException.CardErrorNotAccepted`. + +### Important — dependency and toolchain + - The SDK is now built with **Kotlin 2.2.21**. Host apps compiling with Kotlin **1.9.x** are likely to fail with a Kotlin metadata version mismatch. Upgrade the host Kotlin Gradle plugin to **2.0+** (2.2.x recommended) before integrating 1.1.6. diff --git a/src/lib/openapiContent.ts b/src/lib/openapiContent.ts new file mode 100644 index 00000000..c4cfac3d --- /dev/null +++ b/src/lib/openapiContent.ts @@ -0,0 +1,4 @@ +export const getOperationDescriptionId = (operationId: string) => + `operation:${operationId}`; + +export const getTagDescriptionId = (tagName: string) => `tag:${tagName}`; diff --git a/src/loaders/openapiDescriptions.ts b/src/loaders/openapiDescriptions.ts new file mode 100644 index 00000000..ca7b8b7f --- /dev/null +++ b/src/loaders/openapiDescriptions.ts @@ -0,0 +1,122 @@ +import type { Loader, LoaderContext } from "astro/loaders"; +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { OpenAPIV3, type OpenAPIV3_1 } from "openapi-types"; +import { + getOperationDescriptionId, + getTagDescriptionId, +} from "@lib/openapiContent"; + +const openapiFileUrl = new URL("../../openapi.json", import.meta.url); +const openapiFilePath = fileURLToPath(openapiFileUrl); + +const methods = [ + OpenAPIV3.HttpMethods.GET, + OpenAPIV3.HttpMethods.POST, + OpenAPIV3.HttpMethods.PATCH, + OpenAPIV3.HttpMethods.PUT, + OpenAPIV3.HttpMethods.DELETE, + OpenAPIV3.HttpMethods.OPTIONS, + OpenAPIV3.HttpMethods.HEAD, + OpenAPIV3.HttpMethods.TRACE, +]; + +export function openapiDescriptionsLoader(): Loader { + return { + name: "openapi-descriptions", + async load(context) { + await syncDescriptions(context); + + if (!context.watcher) { + return; + } + + // Imported JSON is cached by the module graph. Watch and reread the file + // explicitly so a synchronized OpenAPI document updates the collection + // without requiring the Astro dev server to be restarted. + context.watcher.add(openapiFilePath); + context.watcher.on("change", async (changedPath) => { + if (changedPath !== openapiFilePath) { + return; + } + + try { + await syncDescriptions(context); + context.logger.info("Reloaded descriptions from openapi.json"); + } catch (error) { + context.logger.error( + `Failed to reload openapi.json: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }); + }, + }; +} + +async function syncDescriptions({ + generateDigest, + parseData, + renderMarkdown, + store, +}: LoaderContext) { + const document = JSON.parse( + await readFile(openapiFileUrl, "utf8"), + ) as OpenAPIV3_1.Document; + + // Parse the document before clearing the store, preserving the previous + // collection if a file watcher observes an incomplete write. + store.clear(); + + for (const tag of document.tags ?? []) { + const id = getTagDescriptionId(tag.name); + const body = tag.description ?? ""; + const data = await parseData({ + id, + data: { kind: "tag", name: tag.name }, + }); + + store.set({ + id, + data, + body, + digest: generateDigest(body), + rendered: await renderMarkdown(body), + }); + } + + for (const [path, pathItem] of Object.entries(document.paths ?? {})) { + if (!pathItem) { + continue; + } + + for (const method of methods) { + const operation = pathItem[method] as + OpenAPIV3_1.OperationObject | undefined; + if (!operation?.operationId) { + continue; + } + + const id = getOperationDescriptionId(operation.operationId); + const body = operation.description ?? ""; + const data = await parseData({ + id, + data: { + kind: "operation", + name: operation.operationId, + method, + path, + }, + }); + + store.set({ + id, + data, + body, + digest: generateDigest(body), + // Route OpenAPI descriptions through Astro's configured Satteri + // processor, including the callout directive plugin. + rendered: await renderMarkdown(body), + }); + } + } +} diff --git a/src/overrides/MarkdownContent.astro b/src/overrides/MarkdownContent.astro index 52b7cb29..e06ee1bf 100644 --- a/src/overrides/MarkdownContent.astro +++ b/src/overrides/MarkdownContent.astro @@ -1,13 +1,13 @@ --- -import Default from "@astrojs/starlight/components/MarkdownContent.astro"; +import MarkdownContent from "@components/MarkdownContent.astro"; --- { Astro.locals.starlightRoute.custom ? ( ) : ( - + - + ) } diff --git a/src/pages/changelog/[slug].astro b/src/pages/changelog/[slug].astro index ac72af36..8e7f3aac 100644 --- a/src/pages/changelog/[slug].astro +++ b/src/pages/changelog/[slug].astro @@ -3,7 +3,7 @@ import { getCollection, render } from "astro:content"; import slugify from "@sindresorhus/slugify"; import { Anchor, Body } from "@sumup-oss/circuit-ui"; import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro"; -import MarkdownContent from "@astrojs/starlight/components/MarkdownContent.astro"; +import MarkdownContent from "@components/MarkdownContent.astro"; import type { StarlightPageProps } from "@astrojs/starlight/props"; export async function getStaticPaths() { diff --git a/src/pages/changelog/index.astro b/src/pages/changelog/index.astro index 8ab6fe91..191c6849 100644 --- a/src/pages/changelog/index.astro +++ b/src/pages/changelog/index.astro @@ -3,7 +3,7 @@ import { getCollection, render } from "astro:content"; import slugify from "@sindresorhus/slugify"; import { Status, Body, Button, Anchor, Headline } from "@sumup-oss/circuit-ui"; import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro"; -import MarkdownContent from "@astrojs/starlight/components/MarkdownContent.astro"; +import MarkdownContent from "@components/MarkdownContent.astro"; import type { StarlightPageProps } from "@astrojs/starlight/props"; const changelogDocs = await getCollection("changelog"); diff --git a/src/pages/changelog/tags/[tag].astro b/src/pages/changelog/tags/[tag].astro index d1264834..11c3a97e 100644 --- a/src/pages/changelog/tags/[tag].astro +++ b/src/pages/changelog/tags/[tag].astro @@ -3,7 +3,7 @@ import { getCollection, render } from "astro:content"; import slugify from "@sindresorhus/slugify"; import { Status, Body, Button, Anchor, Headline } from "@sumup-oss/circuit-ui"; import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro"; -import MarkdownContent from "@astrojs/starlight/components/MarkdownContent.astro"; +import MarkdownContent from "@components/MarkdownContent.astro"; import type { StarlightPageProps } from "@astrojs/starlight/props"; export async function getStaticPaths() { diff --git a/src/pages/help.astro b/src/pages/help.astro index 671755a9..203b22e8 100644 --- a/src/pages/help.astro +++ b/src/pages/help.astro @@ -1,5 +1,6 @@ --- import type { StarlightPageProps } from "@astrojs/starlight/props"; +import MarkdownContent from "@components/MarkdownContent.astro"; import { getCollection, render } from "astro:content"; import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro"; @@ -23,7 +24,9 @@ const posts = await getCollection("help");
{post.data.title}
- + + +
); diff --git a/src/plugins/satteri/callouts.test.ts b/src/plugins/satteri/callouts.test.ts new file mode 100644 index 00000000..e792554f --- /dev/null +++ b/src/plugins/satteri/callouts.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { markdownToHtml } from "satteri"; +import callouts from "./callouts"; + +describe("callouts Markdown plugin", () => { + it("renders a titled callout with nested Markdown", () => { + const source = `:::caution[PCI DSS compliance required] +Use the [official requirements](https://example.com). +:::`; + + const { html } = markdownToHtml(source, { + features: { directive: true }, + mdastPlugins: [callouts], + }); + + expect(html).toContain( + '