Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-10 19:53:38 -06:00
parent 6b10f8b398
commit cfa1e17b3e
5 changed files with 258 additions and 4 deletions

40
APP_STORE_CONNECT.md Normal file
View File

@ -0,0 +1,40 @@
# BusinessCard App Store Connect Copy
## Promotional Text (max 170)
Create polished digital business cards, share instantly with QR codes, and track every connection. Sync cards to Apple Watch and keep contacts organized with notes.
Character count: 164 / 170
## Keywords (max 100)
business card,digital card,qr code,networking,contact manager,vcard,apple watch,lead tracking
Character count: 93 / 100
## Description (max 4000)
BusinessCard helps you create modern digital business cards and share them in seconds.
Build a card that fits your personal brand:
- Add profile, cover, and company logo images
- Choose from multiple header layouts
- Customize themes and card details
- Add unlimited contact fields, social profiles, and links
Share your card anywhere:
- Show a QR code for instant scanning
- Share by text, email, WhatsApp, LinkedIn, and more
- Choose a default card to share quickly
- Enable offline sharing when needed
Stay on top of your network:
- Track who received your card and when
- Save contacts manually or by scanning QR codes
- Add notes, tags, reminder dates, and where you met
- Search contacts fast across names, companies, tags, and notes
Works across your Apple devices:
- iCloud sync keeps cards and contacts up to date on iPhone and iPad
- Apple Watch companion displays your default card QR code for quick sharing
Designed for professionals who want faster follow-up and better relationship tracking after every introduction.
Character count: 1035 / 4000

View File

@ -28,7 +28,7 @@ enum AppIdentifiers {
/// App Clip domain for sharing URLs.
static let appClipDomain: String = {
Bundle.main.object(forInfoDictionaryKey: "AppClipDomain") as? String
?? "cards.example.com"
?? "topdoglabs.com"
}()
// MARK: - Derived Identifiers
@ -52,6 +52,6 @@ enum AppIdentifiers {
/// Generates an App Clip invocation URL for a shared card record.
static func appClipURL(recordName: String) -> URL? {
URL(string: "https://\(appClipDomain)/appclip?id=\(recordName)")
URL(string: "https://\(appClipDomain)/appclip/businesscard?id=\(recordName)")
}
}

View File

@ -31,4 +31,4 @@ CLOUDKIT_CONTAINER_IDENTIFIER = iCloud.$(COMPANY_IDENTIFIER).$(APP_NAME)
// APP CLIP CONFIGURATION
// =============================================================================
APPCLIP_DOMAIN = cards.example.com
APPCLIP_DOMAIN = topdoglabs.com

View File

@ -17,7 +17,7 @@ enum ClipIdentifiers {
/// App Clip domain for URL handling.
static let appClipDomain: String = {
Bundle.main.object(forInfoDictionaryKey: "AppClipDomain") as? String
?? "cards.example.com"
?? "topdoglabs.com"
}()
/// Bundle identifier of the App Clip.

214
Website_AppClip_Plan.md Normal file
View File

@ -0,0 +1,214 @@
# Website + App Clip Plan
## Goal
Set up a production website/domain so the BusinessCard App Clip can be invoked reliably from QR codes and links.
## Website Stack (Provided Context)
- Frontend: React 18 SPA with Vite and `BrowserRouter`
- Hosting: Vercel
- Backend endpoint: Vercel Serverless Function (`send-email.js`)
- Data-driven content from `site.json` and `apps.json`
- App slug on site: `businesscard`
## Confirmed Website Reality (`/website`)
- Domain/site: TopDog Labs (`topdoglabs.com`)
- Routing: SPA routes in `website/src/App.jsx` (`/`, `/apps`, `/apps/:slug`, `/support`, etc.)
- Current Vercel behavior: global rewrite in `website/vercel.json`:
- `/(.*)` -> `/index.html`
- Impact: this current rewrite will also catch `/.well-known/apple-app-site-association` unless you add exclusions.
## Current Project Context
- App currently builds BusinessCard App Clip URLs as:
- `https://<APPCLIP_DOMAIN>/appclip/businesscard?id=<recordName>`
- Domain is configured in `BusinessCard/Configuration/Base.xcconfig`:
- `APPCLIP_DOMAIN = topdoglabs.com`
- App and App Clip entitlements already reference:
- `appclips:$(APPCLIP_DOMAIN)`
## What the Website Must Provide
1. A real HTTPS domain you control.
2. Apple App Site Association (AASA) file at:
- `https://<your-domain>/.well-known/apple-app-site-association`
3. Correct AASA delivery rules:
- No `.json` extension
- `Content-Type: application/json`
- Publicly accessible (no auth)
- No redirects
4. Optional fallback page/endpoint for non-iOS users.
## No Required Language
- There is no required backend language for App Clips.
- You can use static hosting only, as long as AASA is served correctly.
- Use backend/worker only if you want advanced behavior (analytics, non-iOS vCard download, redirects, etc.).
## Does Your Website `businesscard` Slug Matter?
- Not for App Clip verification itself.
- App Clip invocation for this iOS project uses `/appclip/businesscard?id=...`, not `/apps/:slug`.
- Keep `businesscard` slug for your public app page and marketing content.
- Ensure `/appclip/businesscard` and `/.well-known/apple-app-site-association` are not intercepted by SPA rewrites.
## AASA File (Starter)
Create this file on your website:
- Path: `.well-known/apple-app-site-association`
Use this template (replace values):
```json
{
"appclips": {
"apps": [
"TEAM_ID.com.mbrucedogs.BusinessCard.Clip"
]
}
}
```
Notes:
- `TEAM_ID` must match your Apple Developer Team ID.
- Bundle ID must match your App Clip target bundle ID exactly.
## Vercel + React SPA Implementation
When you clone your website repo into this workspace, apply these steps.
1. Add AASA as a static public file:
- `public/.well-known/apple-app-site-association`
2. Ensure your SPA fallback rewrite does not override AASA:
- Do not rewrite `/.well-known/*` to `index.html`
3. Ensure `/appclip/businesscard` is routed intentionally:
- Either serve a dedicated fallback page
- Or allow a specific serverless/edge handler for non-iOS behavior
4. Set explicit response header for AASA:
- `Content-Type: application/json`
### Example `vercel.json` additions
Adjust to merge with your existing config:
```json
{
"cleanUrls": true,
"headers": [
{
"source": "/.well-known/apple-app-site-association",
"headers": [
{ "key": "Content-Type", "value": "application/json" }
]
}
],
"rewrites": [
{ "source": "/.well-known/(.*)", "destination": "/.well-known/$1" },
{ "source": "/appclip/businesscard", "destination": "/appclip.html" },
{ "source": "/appclip", "destination": "/appclip.html" },
{ "source": "/api/(.*)", "destination": "/api/$1" },
{ "source": "/(.*)", "destination": "/index.html" }
]
}
```
Notes:
- Keep `/.well-known/*` before catch-all SPA rewrite.
- Keep `/api/*` callable (your contact form endpoint).
- If you want `/appclip/businesscard` to show a fallback page, add `website/public/appclip.html`.
## Minimal Files To Add in `website/public`
1. `website/public/.well-known/apple-app-site-association`
2. `website/public/appclip.html` (optional but recommended fallback)
`appclip.html` can be minimal:
- Message for iPhone users to open via camera/App Clip card
- Link for non-iOS users (for example to app page `/apps/businesscard` or App Store URL)
- Optional JS to parse `id` query parameter and show it for support/debug
## Multi-App App Clip Pattern (Recommended)
For multiple apps, use one shared domain and namespaced paths:
- BusinessCard: `/appclip/businesscard?id=<id>`
- Rituals: `/appclip/rituals?id=<id>`
- SelfieCam: `/appclip/selfie-cam?id=<id>`
Keep one AASA file and include all App Clip bundle IDs:
```json
{
"appclips": {
"apps": [
"TEAM_ID.com.mbrucedogs.BusinessCard.Clip",
"TEAM_ID.com.mbrucedogs.Rituals.Clip",
"TEAM_ID.com.mbrucedogs.SelfieCam.Clip"
]
}
}
```
Then add one rewrite per App Clip path in `vercel.json`.
## Domain + Hosting Checklist
1. Pick subdomain (recommended): `cards.yourdomain.com`
2. Configure DNS record to your host.
3. Ensure valid TLS certificate (HTTPS).
4. Add/upload AASA file at `/.well-known/apple-app-site-association`.
5. Verify no redirect occurs for that file.
## App Project Changes Required
1. Update domain in `BusinessCard/Configuration/Base.xcconfig`:
- `APPCLIP_DOMAIN = cards.yourdomain.com` (recommended)
- or use an existing domain if preferred (for example a subpath on `topdoglabs.com`)
2. Confirm entitlements still resolve correctly:
- iOS app: `BusinessCard/BusinessCard.entitlements`
- App Clip: `BusinessCardClip/BusinessCardClip.entitlements`
3. Clean and rebuild in Xcode after config changes.
Note:
- A dedicated subdomain (like `cards.topdoglabs.com`) is usually cleaner for App Clip + future deep link infrastructure.
## App Store Connect Steps
1. Open your app in App Store Connect.
2. Create/configure App Clip Experience.
3. Add invocation URL using your real domain/path (for BusinessCard:
`/appclip/businesscard`).
4. Submit for review with App Clip metadata.
## Verification Steps (Before Release)
1. Browser check:
- Open `https://<your-domain>/.well-known/apple-app-site-association`
- Confirm file returns directly (no redirect/auth).
2. Header check (terminal):
```bash
curl -I https://<your-domain>/.well-known/apple-app-site-association
```
Confirm:
- `HTTP 200`
- `content-type: application/json`
3. Device test:
- Install app + App Clip build on physical iPhone.
- Generate App Clip QR in app share screen.
- Scan QR and confirm App Clip opens.
- Confirm App Clip can fetch CloudKit card and save contact.
4. Confirm SPA still works:
- `/`
- `/apps`
- `/apps/businesscard`
- `/support`
- `/.well-known/apple-app-site-association`
- `/appclip/businesscard?id=test`
## Optional Fallback (Recommended)
For Android/desktop scans:
- Host a simple web page or download flow at `/appclip/businesscard`.
- Show contact preview or provide `.vcf` download if desired.
## Suggested Rollout Order
1. Domain + AASA live
2. Update `APPCLIP_DOMAIN`
3. Rebuild + physical-device testing
4. App Store Connect App Clip Experience
5. Release
## Risks / Gotchas
- Wrong Team ID or bundle ID in AASA breaks invocation.
- Redirecting `/.well-known/apple-app-site-association` breaks verification.
- Simulator is unreliable for full App Clip invocation testing.
- Keep CloudKit schema and public read permissions aligned with App Clip fetch flow.