diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index c918c9a34..966e6b399 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -12,10 +12,61 @@ on:
permissions:
contents: read
+concurrency:
+ group: codeql-${{ github.event.pull_request.number || github.run_id }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
env:
XCODE_VERSION: 16.4
jobs:
+ codeql-scope:
+ name: Detect CodeQL Scope
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ outputs:
+ swift_core: ${{ github.event_name != 'pull_request' || steps.core.outputs.swift_core == 'true' }}
+ swift_wrappers: ${{ github.event_name != 'pull_request' || steps.wrappers.outputs.changes != '[]' }}
+ swift_components: >-
+ ${{ github.event_name != 'pull_request' &&
+ '["react-native","expo","expo-onside","flutter","godot"]' ||
+ steps.wrappers.outputs.changes }}
+ steps:
+ - name: Checkout
+ if: github.event_name == 'pull_request'
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ persist-credentials: false
+
+ - name: Detect Apple core changes
+ if: github.event_name == 'pull_request'
+ id: core
+ uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
+ with:
+ filters: |
+ swift_core:
+ - 'packages/apple/**'
+ - 'libraries/kmp-iap/native/InAppPurchaseBridge/**'
+
+ - name: Detect Swift wrapper changes
+ if: github.event_name == 'pull_request'
+ id: wrappers
+ uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
+ with:
+ filters: |
+ react-native:
+ - 'libraries/react-native-iap/**'
+ expo:
+ - 'libraries/expo-iap/**'
+ expo-onside:
+ - 'libraries/expo-iap/**'
+ flutter:
+ - 'libraries/flutter_inapp_purchase/**'
+ godot:
+ - 'libraries/godot-iap/**'
+
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
@@ -225,6 +276,8 @@ jobs:
analyze-swift:
name: Analyze (swift)
+ needs: codeql-scope
+ if: needs.codeql-scope.outputs.swift_core == 'true'
runs-on: macos-15
timeout-minutes: 45
permissions:
@@ -267,8 +320,12 @@ jobs:
analyze-swift-wrappers:
name: Analyze (swift / ${{ matrix.component }})
- if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
- runs-on: ${{ matrix.runner }}
+ needs: codeql-scope
+ if: >-
+ needs.codeql-scope.outputs.swift_wrappers == 'true' &&
+ (github.event_name != 'pull_request' ||
+ github.event.pull_request.head.repo.full_name == github.repository)
+ runs-on: ${{ (github.event_name == 'pull_request' || matrix.component == 'godot') && 'macos-26' || 'xcode-27' }}
timeout-minutes: 90
permissions:
contents: read
@@ -276,17 +333,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- include:
- - component: react-native
- runner: xcode-27
- - component: expo
- runner: xcode-27
- - component: expo-onside
- runner: xcode-27
- - component: flutter
- runner: xcode-27
- - component: godot
- runner: macos-26
+ component: ${{ fromJSON(needs.codeql-scope.outputs.swift_components) }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
diff --git a/packages/docs/COMMUNITY_RESOURCES.md b/packages/docs/COMMUNITY_RESOURCES.md
index 0e3f3d114..d6be114ac 100644
--- a/packages/docs/COMMUNITY_RESOURCES.md
+++ b/packages/docs/COMMUNITY_RESOURCES.md
@@ -2,7 +2,7 @@
The [Community Resources page](https://openiap.dev/community-resources) is a
curated library of third-party knowledge about OpenIAP and the implementations
-built on its specification. The official OpenIAP blog is presented separately
+built on its specification. Official OpenIAP updates are presented separately
from this community-maintained collection.
Thank you to every developer, writer, speaker, and team who shares practical
@@ -53,10 +53,13 @@ these rules:
them.
5. Add accurate language metadata. English is the initial default; future
non-English additions should ship with a visible language filter.
-6. Do not add view counts or other metrics that will become stale.
+6. Add a verified `publishedAt` date in `YYYY-MM-DD` format. Use the original
+ publication date when available. For sources that only expose an update or
+ submission date, set `dateLabel` to `Updated` or `Submitted`.
+7. Do not add view counts or other metrics that will become stale.
Before opening a pull request, run the docs format check, typecheck, and build
described in [`README.md`](README.md).
If you prefer to introduce your work before editing the library, share it in
-[Show and Tell](https://github.com/hyodotdev/openiap/discussions/categories/show-and-tell).
+the [community resources discussion](https://github.com/hyodotdev/openiap/discussions/349).
diff --git a/packages/docs/SHOWCASE.md b/packages/docs/SHOWCASE.md
index fdd610045..6b804a447 100644
--- a/packages/docs/SHOWCASE.md
+++ b/packages/docs/SHOWCASE.md
@@ -68,11 +68,11 @@ are skipped at render time.
## Don't want to send a PR?
-Comment on [issue #280](https://github.com/hyodotdev/openiap/issues/280) or
+Reply to [discussion #350](https://github.com/hyodotdev/openiap/discussions/350) or
email **hyo@hyo.dev** with your app name, one-liner, logo, store links, and which
library you use — we'll add it for you.
## Removal and updates
Your app is listed only with your permission. To change or remove an entry, open
-a PR, comment on issue #280, or email hyo@hyo.dev anytime.
+a PR, comment on discussion #350, or email hyo@hyo.dev anytime.
diff --git a/packages/docs/src/components/MetaWordmark.tsx b/packages/docs/src/components/MetaWordmark.tsx
new file mode 100644
index 000000000..812456a3f
--- /dev/null
+++ b/packages/docs/src/components/MetaWordmark.tsx
@@ -0,0 +1,12 @@
+import type { ReactElement } from 'react';
+
+function MetaWordmark(): ReactElement {
+ return (
+
+
+
+
+ );
+}
+
+export default MetaWordmark;
diff --git a/packages/docs/src/components/ShowcaseCards.tsx b/packages/docs/src/components/ShowcaseCards.tsx
index 3a0ee2047..74d62e302 100644
--- a/packages/docs/src/components/ShowcaseCards.tsx
+++ b/packages/docs/src/components/ShowcaseCards.tsx
@@ -3,8 +3,8 @@ import { SiApple, SiGoogleplay } from 'react-icons/si';
import { Globe } from 'lucide-react';
import type { ShowcaseApp } from '../lib/showcase';
-export const SHOWCASE_ISSUE_URL =
- 'https://github.com/hyodotdev/openiap/issues/280';
+export const SHOWCASE_DISCUSSION_URL =
+ 'https://github.com/hyodotdev/openiap/discussions/350';
export const SHOWCASE_GUIDE_URL =
'https://github.com/hyodotdev/openiap/blob/main/packages/docs/SHOWCASE.md';
@@ -159,7 +159,7 @@ export function ShowcaseSubmitCard() {
Send your app name, icon, and store links — we'll add it here.
getEcosystemMeta(ecosystem).label
- ),
+ ...resource.ecosystems.flatMap((ecosystem) => {
+ const meta = getEcosystemMeta(ecosystem);
+ return [meta.label, meta.module];
+ }),
]
.filter((value): value is string => Boolean(value))
.some((value) => value.toLowerCase().includes(normalizedQuery));
}
function ResourceRow({ resource }: ResourceRowProps) {
- const ecosystemLabels = resource.ecosystems
- .map((ecosystem) => getEcosystemMeta(ecosystem).label)
+ const ecosystemModules = resource.ecosystems
+ .map((ecosystem) => getEcosystemMeta(ecosystem).module)
.join(' · ');
return (
@@ -208,8 +213,16 @@ function ResourceRow({ resource }: ResourceRowProps) {
{resource.author && resource.organization && (
{resource.organization}
)}
+
+
+
{resource.platform}
- {ecosystemLabels}
+ {ecosystemModules}
{resource.seriesLabel && {resource.seriesLabel}}
@@ -341,7 +354,7 @@ function CommunityResources() {
-
+
@@ -350,7 +363,7 @@ function CommunityResources() {
Products shipping with OpenIAP
-
+
@@ -382,7 +395,7 @@ function CommunityResources() {
strokeWidth={1.9}
aria-hidden="true"
/>
- Official blog
+ Official updates
{resource.platform}
@@ -413,6 +426,19 @@ function CommunityResources() {
Community resource library
+
+
+
+ Submit a community resource
+ GitHub Discussion
+
+
+
These third-party resources were written for different
@@ -453,7 +479,7 @@ function CommunityResources() {
-
-
-
- Shipping with OpenIAP? Add your app icon and
- store links to the community index.
-
-
- Add your app
-
-
-
@@ -552,7 +553,7 @@ function Home() {
-
+
02
Apps built with OpenIAP
@@ -602,7 +603,7 @@ function Home() {
rel="noopener noreferrer"
aria-label="Visit Meta"
>
-

+
Angel
diff --git a/packages/docs/src/pages/showcase.tsx b/packages/docs/src/pages/showcase.tsx
index 35c7a74dd..dd1e22bec 100644
--- a/packages/docs/src/pages/showcase.tsx
+++ b/packages/docs/src/pages/showcase.tsx
@@ -3,7 +3,7 @@ import {
ShowcaseAppCard,
ShowcaseSubmitCard,
SHOWCASE_GUIDE_URL,
- SHOWCASE_ISSUE_URL,
+ SHOWCASE_DISCUSSION_URL,
showcaseGridStyle,
} from '../components/ShowcaseCards';
import { SHOWCASE_APPS } from '../lib/showcase';
@@ -48,14 +48,14 @@ function Showcase() {
marginTop: '0.5rem',
}}
>
- Comment on{' '}
+ Reply to{' '}
- the showcase issue
+ the showcase discussion
{' '}
with the details below and we'll add your app. Prefer a pull
request? Add an entry to{' '}
diff --git a/packages/docs/src/pages/sponsors.tsx b/packages/docs/src/pages/sponsors.tsx
index c73219de7..a6c01ffa9 100644
--- a/packages/docs/src/pages/sponsors.tsx
+++ b/packages/docs/src/pages/sponsors.tsx
@@ -6,6 +6,7 @@ import {
ShieldCheck,
Smartphone,
} from 'lucide-react';
+import MetaWordmark from '../components/MetaWordmark';
import SEO from '../components/SEO';
import { LIBRARIES, LIBRARY_IMAGES } from '../lib/images';
@@ -35,32 +36,13 @@ const FUNDING_LINES = [
},
] as const;
-interface CurrentSponsor {
- name: string;
- image: string;
- url: string;
-}
-
-const CURRENT_SPONSORS: CurrentSponsor[] = [
+const CURRENT_SPONSORS = [
{
name: 'Meta',
- image: '/sponsors/meta.webp',
+ logo: ,
url: 'https://meta.com',
},
-];
-
-const REACT_NATIVE_FAMILY = LIBRARIES.filter(
- (library) =>
- library.name === 'react-native-iap' || library.name === 'expo-iap'
-).sort((first, second) => {
- if (first.name === second.name) return 0;
- return first.name === 'react-native-iap' ? -1 : 1;
-});
-
-const OTHER_FRAMEWORKS = LIBRARIES.filter(
- (library) =>
- library.name !== 'react-native-iap' && library.name !== 'expo-iap'
-);
+] as const;
function Sponsors() {
return (
@@ -134,7 +116,7 @@ function Sponsors() {
rel="noopener noreferrer"
aria-label={`Visit ${sponsor.name}`}
>
-
+ {sponsor.logo}
))}
@@ -190,35 +172,7 @@ function Sponsors() {
/>
Google
-
- {OTHER_FRAMEWORKS.map((library) => (
+ {LIBRARIES.map((library) => (
img {
+ width: auto;
+ height: 100%;
+ object-fit: contain;
+}
+
img[src='/frameworks/apple.svg'] {
filter: var(--apple-logo-filter);
}
diff --git a/packages/docs/src/styles/community-resources.css b/packages/docs/src/styles/community-resources.css
index 866ba881a..cf53a7cfa 100644
--- a/packages/docs/src/styles/community-resources.css
+++ b/packages/docs/src/styles/community-resources.css
@@ -565,6 +565,55 @@
line-height: var(--leading-display);
}
+.cr-apps-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0 0.65rem;
+}
+
+.cr-discussion-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.55rem;
+ margin-top: 0.9rem;
+ padding: 0.52rem 0.66rem;
+ border: 1px solid var(--border-color);
+ border-radius: 0.55rem;
+ color: var(--text-primary);
+ background: var(--bg-secondary);
+ text-decoration: none;
+ transition:
+ border-color 150ms ease,
+ transform 150ms ease;
+}
+
+.cr-discussion-link > svg {
+ flex: 0 0 auto;
+ color: var(--cr-accent);
+}
+
+.cr-discussion-link > span {
+ display: flex;
+ flex-direction: column;
+ gap: 0.08rem;
+}
+
+.cr-discussion-link strong {
+ font-size: 0.72rem;
+ line-height: 1.25;
+}
+
+.cr-discussion-link small {
+ color: var(--text-secondary);
+ font-size: 0.59rem;
+ line-height: 1.25;
+}
+
+.cr-discussion-link:hover {
+ border-color: var(--cr-accent);
+ transform: translateY(-1px);
+}
+
.cr-apps-heading > p {
margin: 0;
color: var(--text-secondary);
@@ -634,15 +683,7 @@
font-weight: 680;
}
-.cr-app-meta > div {
- display: flex;
- flex-wrap: wrap;
- justify-content: flex-end;
- gap: 0.55rem;
-}
-
-.cr-app-meta a,
-.cr-app-submit a {
+.cr-app-meta a {
display: inline-flex;
align-items: center;
gap: 0.18rem;
@@ -652,31 +693,11 @@
text-decoration: none;
}
-.cr-app-meta a:hover,
-.cr-app-submit a:hover {
+.cr-app-meta a:hover {
text-decoration: underline;
text-underline-offset: 0.18em;
}
-.cr-app-submit {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 2rem;
- padding-top: 1rem;
-}
-
-.cr-app-submit p {
- margin: 0;
- color: var(--text-secondary);
- font-size: 0.72rem;
- line-height: 1.5;
-}
-
-.cr-app-submit strong {
- color: var(--text-primary);
-}
-
.cr-contribute {
display: flex;
align-items: center;
@@ -875,12 +896,6 @@
align-items: flex-start;
flex-direction: column;
}
-
- .cr-app-submit {
- align-items: flex-start;
- flex-direction: column;
- gap: 0.6rem;
- }
}
@media (prefers-reduced-motion: reduce) {
diff --git a/packages/docs/src/styles/explore-pages.css b/packages/docs/src/styles/explore-pages.css
index 395cfe900..325f9accd 100644
--- a/packages/docs/src/styles/explore-pages.css
+++ b/packages/docs/src/styles/explore-pages.css
@@ -771,6 +771,7 @@
.xp-sponsor-node:not(.xp-sponsor-open-node) > a {
display: inline-block;
+ filter: drop-shadow(0 0.7rem 1.2rem rgba(45, 25, 20, 0.08));
line-height: 0;
transition: transform 180ms ease;
}
@@ -779,12 +780,9 @@
transform: translateY(-3px) scale(1.025);
}
-.xp-sponsor-node img {
- width: auto;
+.xp-sponsor-node .meta-wordmark {
max-width: 12rem;
height: 3rem;
- object-fit: contain;
- filter: drop-shadow(0 0.7rem 1.2rem rgba(45, 25, 20, 0.08));
}
.xp-sponsor-open-node > a {
@@ -819,8 +817,7 @@
border-block: 1px solid var(--border-color);
}
-.xp-stack-rail > a,
-.xp-stack-family > a {
+.xp-stack-rail > a {
display: flex;
min-width: 0;
flex-direction: column;
@@ -851,54 +848,6 @@
color: var(--xp-accent);
}
-.xp-stack-family {
- display: grid;
- min-width: 0;
- grid-column: span 2;
- grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.75fr);
- border-left: 1px solid var(--border-color);
- background: rgba(220, 104, 67, 0.025);
-}
-
-.xp-stack-family > a {
- flex-direction: row;
- gap: 0.7rem;
- align-items: center;
- justify-content: center;
- text-align: left;
-}
-
-.xp-stack-family > a + a {
- border-left: 1px solid var(--border-color);
-}
-
-.xp-stack-family > a > span {
- display: flex;
- min-width: 0;
- flex-direction: column;
- gap: 0.15rem;
-}
-
-.xp-stack-family strong {
- overflow: hidden;
- color: var(--text-primary);
- font-size: 0.64rem;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.xp-stack-family small {
- color: var(--text-secondary);
- font-size: 0.5rem;
- letter-spacing: 0.04em;
- text-transform: uppercase;
-}
-
-.xp-stack-family-secondary img {
- width: 1.75rem;
- height: 1.75rem;
-}
-
.xp-funding-ledger {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -1108,11 +1057,11 @@
grid-template-columns: repeat(4, minmax(0, 1fr));
}
- .xp-stack-rail > a:nth-child(4) {
+ .xp-stack-rail > a:nth-child(5) {
border-left: 0;
}
- .xp-stack-rail > a:nth-child(n + 4) {
+ .xp-stack-rail > a:nth-child(n + 5) {
border-top: 1px solid var(--border-color);
}
@@ -1283,7 +1232,7 @@
gap: 1rem;
}
- .xp-sponsor-node img {
+ .xp-sponsor-node .meta-wordmark {
max-width: 8.75rem;
height: 2.35rem;
}
diff --git a/packages/docs/src/styles/home.css b/packages/docs/src/styles/home.css
index 3dc924b7c..ed58e7fde 100644
--- a/packages/docs/src/styles/home.css
+++ b/packages/docs/src/styles/home.css
@@ -548,10 +548,8 @@
gap: 0.2rem;
}
-.hero-backer img {
- width: auto;
+.hero-backer .meta-wordmark {
height: 1.35rem;
- object-fit: contain;
}
@media (max-width: 1050px) {
@@ -1515,8 +1513,7 @@ a.spec-item:hover code {
transform: scale(1.035);
}
-.home-sponsor-logo-link img {
- width: auto;
+.home-sponsor-logo-link .meta-wordmark {
height: 4.1rem;
}
@@ -1668,7 +1665,7 @@ a.spec-item:hover code {
padding-inline: 1rem;
}
- .home-sponsor-logo-link img {
+ .home-sponsor-logo-link .meta-wordmark {
height: 2.5rem;
}
diff --git a/scripts/audit-security.test.mjs b/scripts/audit-security.test.mjs
index ce5971cf4..ea7cb7fa4 100644
--- a/scripts/audit-security.test.mjs
+++ b/scripts/audit-security.test.mjs
@@ -568,20 +568,60 @@ test("compiled CodeQL Gradle builds reuse the transient-network retry guard", ()
assert.match(workflow, /:library:compilePlayDebugKotlinAndroid/u);
});
-test("CodeQL never runs pull-request code on private Xcode runners", () => {
+test("CodeQL scopes Swift pull requests to public macOS runners", () => {
const workflow = readFileSync(
new URL("../.github/workflows/codeql.yml", import.meta.url),
"utf8",
);
+ const scope = workflow.slice(
+ workflow.indexOf(" codeql-scope:"),
+ workflow.indexOf(" analyze:"),
+ );
const wrappers = workflow.slice(
workflow.indexOf(" analyze-swift-wrappers:"),
);
+ const swiftCore = workflow.slice(
+ workflow.indexOf(" analyze-swift:"),
+ workflow.indexOf(" analyze-swift-wrappers:"),
+ );
+ assert.match(
+ workflow,
+ /group: codeql-\$\{\{ github\.event\.pull_request\.number \|\| github\.run_id \}\}/u,
+ );
+ assert.match(
+ workflow,
+ /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/u,
+ );
+ assert.match(scope, /swift_core:/u);
+ assert.match(swiftCore, /needs: codeql-scope/u);
+ assert.match(
+ swiftCore,
+ /if: needs\.codeql-scope\.outputs\.swift_core == 'true'/u,
+ );
+ assert.match(scope, /react-native:/u);
+ assert.match(scope, /expo-onside:/u);
+ assert.match(scope, /flutter:/u);
+ assert.match(scope, /godot:/u);
+ assert.match(
+ scope,
+ /\["react-native","expo","expo-onside","flutter","godot"\]/u,
+ );
+ assert.match(
+ wrappers,
+ /needs\.codeql-scope\.outputs\.swift_wrappers == 'true'/u,
+ );
+ assert.match(
+ wrappers,
+ /github\.event\.pull_request\.head\.repo\.full_name == github\.repository/u,
+ );
+ assert.match(
+ wrappers,
+ /runs-on: \$\{\{ \(github\.event_name == 'pull_request' \|\| matrix\.component == 'godot'\) && 'macos-26' \|\| 'xcode-27' \}\}/u,
+ );
assert.match(
wrappers,
- /if: github\.event_name != 'pull_request' \|\| github\.event\.pull_request\.head\.repo\.full_name == github\.repository/u,
+ /component: \$\{\{ fromJSON\(needs\.codeql-scope\.outputs\.swift_components\) \}\}/u,
);
- assert.match(wrappers, /runner: xcode-27/u);
- assert.match(wrappers, /component: expo-onside/u);
assert.match(
wrappers,
/EXPO_IAP_ONSIDE: \$\{\{ matrix\.component == 'expo-onside' && '1' \|\| '0' \}\}/u,