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

+ +

These third-party resources were written for different @@ -453,7 +479,7 @@ function CommunityResources() {

@@ -581,15 +607,38 @@ function CommunityResources() {

Apps in the wild

Built with OpenIAP

+
+ +

- Products using OpenIAP libraries across iOS, Android, and the - web. + A preview of products using OpenIAP libraries. Open the showcase + for the full list and store links.

- {SHOWCASE_APPS.map((app, index) => ( + {FEATURED_SHOWCASE_APPS.map((app, index) => (
{app.library} -
- {app.ios ? ( - - iOS - ) : null} - {app.android ? ( - - Android - ) : null} - {app.web ? ( - - Web - ) : null} -
+ + View showcase +
))}
- -
-

- Shipping with OpenIAP? Add your app icon and - store links to the community index. -

- - Add your app - -
@@ -670,15 +697,6 @@ function CommunityResources() { Edit and open a PR
@@ -552,7 +553,7 @@ function Home() {
Apps built with OpenIAP @@ -602,7 +603,7 @@ function Home() { rel="noopener noreferrer" aria-label="Visit Meta" > - 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.name} + {sponsor.logo} ))} @@ -190,35 +172,7 @@ function Sponsors() { /> Google -
- {REACT_NATIVE_FAMILY.map((library) => ( - - - - {library.homeLabel} - - {library.name === 'react-native-iap' - ? 'Primary' - : 'Same API'} - - - - ))} -
- {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,