Skip to content

Commit d38f91d

Browse files
committed
Merge remote-tracking branch 'origin/main' into engin/remove-local-mcp
2 parents 1be5353 + 28c138d commit d38f91d

File tree

122 files changed

+7345
-1094
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

122 files changed

+7345
-1094
lines changed

.github/workflows/lint.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ jobs:
1212
runs-on: ubuntu-latest
1313
steps:
1414
- name: Checkout Actions Repository
15-
uses: actions/checkout@v5
15+
uses: actions/checkout@v6
1616
- name: Spell Check Repo
17-
uses: crate-ci/typos@v1.39.0
17+
uses: crate-ci/typos@v1.42.0

.github/workflows/main.yml

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
name: Auto dev-docs Commenter
22

33
on:
4-
pull_request:
4+
pull_request_target:
55
types: [opened]
66

77
jobs:
88
comment:
99
runs-on: ubuntu-latest
10+
permissions:
11+
deployments: write
1012
steps:
11-
- name: Add preview URL to PR
13+
- name: Create preview deployment annotation
1214
uses: actions/github-script@v8
1315
with:
1416
github-token: ${{secrets.GITHUB_TOKEN}}
1517
script: |
16-
const issue_number = context.issue.number;
17-
const message = `Preview this PR here: https://dev-docs.revenuecat.com/pr-${issue_number}/`;
18-
github.rest.issues.createComment({
19-
...context.repo,
20-
issue_number: issue_number,
21-
body: message
18+
const prNumber = context.payload.pull_request.number;
19+
const headSha = context.payload.pull_request.head.sha;
20+
const previewUrl = `https://dev-docs.revenuecat.com/pr-${prNumber}/`;
21+
22+
// Create deployment
23+
const deployment = await github.rest.repos.createDeployment({
24+
owner: context.repo.owner,
25+
repo: context.repo.repo,
26+
ref: headSha,
27+
environment: `preview-pr-${prNumber}`,
28+
description: `Preview deployment for PR #${prNumber}`,
29+
auto_merge: false,
30+
required_contexts: []
31+
});
32+
33+
// Create deployment status with preview URL
34+
await github.rest.repos.createDeploymentStatus({
35+
owner: context.repo.owner,
36+
repo: context.repo.repo,
37+
deployment_id: deployment.data.id,
38+
state: 'success',
39+
environment_url: previewUrl,
40+
description: `Preview available at ${previewUrl}`
2241
});

.tool-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
nodejs 24.7.0
1+
nodejs 24.11.0

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ Page({ slug: "installation/ios" });
201201
// References: /docs/installation/ios.md or ios.mdx
202202
```
203203

204-
The final path is constructed as: `itemsPathPrefix + slug`
204+
The final path is constructed as: `itemsPathPrefix + slug`. New documentation pages are not automatically added to any sidebars - you must add them under a category (or subcategory.)
205205

206206
### Link Configuration
207207

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Apple App Store: Extend a subscription renewal date
2+
const response = await fetch(
3+
`https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(
4+
appUserId
5+
)}/subscriptions/${transactionId}/extend`,
6+
{
7+
method: "POST",
8+
headers: {
9+
Authorization: `Bearer ${REVENUECAT_V1_SECRET_API_KEY}`,
10+
"Content-Type": "application/json",
11+
},
12+
body: JSON.stringify({
13+
extend_by_days: 30, // 1-90 days
14+
extend_reason_code: 1, // 1 = Customer Satisfaction
15+
}),
16+
}
17+
);
18+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Google Play Store: Defer a subscription renewal date
2+
3+
// Extract subscription ID (before the colon) from product_id
4+
// e.g., "subscription_id:base_plan_id" -> "subscription_id"
5+
const subscriptionId = productId.split(":")[0];
6+
7+
const response = await fetch(
8+
`https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(
9+
appUserId
10+
)}/subscriptions/${encodeURIComponent(subscriptionId)}/defer`,
11+
{
12+
method: "POST",
13+
headers: {
14+
Authorization: `Bearer ${REVENUECAT_V1_SECRET_API_KEY}`,
15+
"Content-Type": "application/json",
16+
},
17+
body: JSON.stringify({
18+
extend_by_days: 30, // 1-365 days
19+
}),
20+
}
21+
);
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import express, { Request, Response } from "express";
2+
3+
const app = express();
4+
app.use(express.json());
5+
6+
// Your RevenueCat Secret API Key (v1)
7+
const REVENUECAT_API_KEY = process.env.REVENUECAT_SECRET_API_KEY!;
8+
const WEBHOOK_AUTH_HEADER = process.env.WEBHOOK_AUTH_HEADER!;
9+
10+
// Configure your promotional extension settings
11+
const PROMO_CONFIG = {
12+
// Offering IDs that qualify for the promotional extension
13+
eligibleOfferingIds: ["holiday_promo", "partner_deal", "retention_offer"],
14+
// Number of days to extend the subscription
15+
extensionDays: 30,
16+
// Apple extension reason code (1 = Customer Satisfaction)
17+
appleReasonCode: 1,
18+
};
19+
20+
interface WebhookEvent {
21+
event: {
22+
type: string;
23+
app_user_id: string;
24+
original_transaction_id: string;
25+
transaction_id: string;
26+
store: string;
27+
product_id: string;
28+
presented_offering_id: string | null;
29+
environment: string;
30+
};
31+
api_version: string;
32+
}
33+
34+
// Webhook endpoint to receive RevenueCat events
35+
app.post(
36+
"/webhooks/revenuecat",
37+
async (req: Request, res: Response): Promise<void> => {
38+
// Verify the authorization header
39+
const authHeader = req.headers.authorization;
40+
if (authHeader !== WEBHOOK_AUTH_HEADER) {
41+
res.status(401).json({ error: "Unauthorized" });
42+
return;
43+
}
44+
45+
// Respond immediately to avoid timeout
46+
res.status(200).json({ received: true });
47+
48+
// Process the webhook asynchronously
49+
const webhookData: WebhookEvent = req.body;
50+
await processWebhook(webhookData);
51+
}
52+
);
53+
54+
async function processWebhook(webhookData: WebhookEvent): Promise<void> {
55+
const { event } = webhookData;
56+
57+
// Only process initial purchases
58+
if (event.type !== "INITIAL_PURCHASE") {
59+
console.log(`Skipping event type: ${event.type}`);
60+
return;
61+
}
62+
63+
// Check if this purchase qualifies for promotional extension
64+
if (!isEligibleForPromoExtension(event)) {
65+
console.log(`Purchase not eligible for promotional extension`);
66+
return;
67+
}
68+
69+
try {
70+
// Apply the extension based on the store platform
71+
await applyPromotionalExtension(event);
72+
console.log(
73+
`Successfully applied promotional extension for user: ${event.app_user_id}`
74+
);
75+
} catch (error) {
76+
console.error(`Failed to apply promotional extension:`, error);
77+
// Implement your error handling/retry logic here
78+
}
79+
}
80+
81+
function isEligibleForPromoExtension(event: WebhookEvent["event"]): boolean {
82+
// Check if the offering qualifies for the promotion
83+
if (!event.presented_offering_id) {
84+
return false;
85+
}
86+
87+
return PROMO_CONFIG.eligibleOfferingIds.includes(event.presented_offering_id);
88+
}
89+
90+
async function applyPromotionalExtension(
91+
event: WebhookEvent["event"]
92+
): Promise<void> {
93+
const { store, app_user_id, transaction_id, product_id } = event;
94+
95+
switch (store) {
96+
case "APP_STORE":
97+
case "MAC_APP_STORE":
98+
await extendAppleSubscription(app_user_id, transaction_id);
99+
break;
100+
101+
case "PLAY_STORE":
102+
// Extract subscription ID (before the colon) from product_id
103+
// e.g., "subscription_id:base_plan_id" -> "subscription_id"
104+
const subscriptionId = product_id.split(":")[0];
105+
await deferGoogleSubscription(app_user_id, subscriptionId);
106+
break;
107+
108+
default:
109+
console.log(`Store ${store} does not support subscription extensions`);
110+
}
111+
}
112+
113+
async function extendAppleSubscription(
114+
appUserId: string,
115+
transactionId: string
116+
): Promise<void> {
117+
const url = `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(
118+
appUserId
119+
)}/subscriptions/${transactionId}/extend`;
120+
121+
const response = await fetch(url, {
122+
method: "POST",
123+
headers: {
124+
Authorization: `Bearer ${REVENUECAT_API_KEY}`,
125+
"Content-Type": "application/json",
126+
},
127+
body: JSON.stringify({
128+
extend_by_days: PROMO_CONFIG.extensionDays,
129+
extend_reason_code: PROMO_CONFIG.appleReasonCode,
130+
}),
131+
});
132+
133+
if (!response.ok) {
134+
const errorBody = await response.text();
135+
throw new Error(
136+
`Apple extension failed: ${response.status} - ${errorBody}`
137+
);
138+
}
139+
140+
console.log(`Extended Apple subscription by ${PROMO_CONFIG.extensionDays} days`);
141+
}
142+
143+
async function deferGoogleSubscription(
144+
appUserId: string,
145+
subscriptionId: string
146+
): Promise<void> {
147+
const url = `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(
148+
appUserId
149+
)}/subscriptions/${encodeURIComponent(subscriptionId)}/defer`;
150+
151+
const response = await fetch(url, {
152+
method: "POST",
153+
headers: {
154+
Authorization: `Bearer ${REVENUECAT_API_KEY}`,
155+
"Content-Type": "application/json",
156+
},
157+
body: JSON.stringify({
158+
extend_by_days: PROMO_CONFIG.extensionDays,
159+
}),
160+
});
161+
162+
if (!response.ok) {
163+
const errorBody = await response.text();
164+
throw new Error(
165+
`Google deferral failed: ${response.status} - ${errorBody}`
166+
);
167+
}
168+
169+
console.log(`Deferred Google subscription by ${PROMO_CONFIG.extensionDays} days`);
170+
}
171+
172+
const PORT = process.env.PORT || 3000;
173+
app.listen(PORT, () => {
174+
console.log(`Webhook server listening on port ${PORT}`);
175+
});
176+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
async function applyPromotionalExtension(
2+
event: WebhookEvent["event"]
3+
): Promise<void> {
4+
const { store, app_user_id, transaction_id, product_id } = event;
5+
6+
switch (store) {
7+
case "APP_STORE":
8+
case "MAC_APP_STORE":
9+
// Apple subscriptions use the extend endpoint with transaction ID
10+
await extendAppleSubscription(app_user_id, transaction_id);
11+
break;
12+
13+
case "PLAY_STORE":
14+
// Google subscriptions use the defer endpoint with subscription ID
15+
// Extract subscription ID (before the colon) from product_id
16+
const subscriptionId = product_id.split(":")[0];
17+
await deferGoogleSubscription(app_user_id, subscriptionId);
18+
break;
19+
20+
case "STRIPE":
21+
case "RC_BILLING":
22+
// Stripe and Web Billing don't support direct extensions via RevenueCat
23+
// You would need to use Stripe's API directly or grant an entitlement
24+
console.log(`${store} requires direct integration for extensions`);
25+
break;
26+
27+
case "AMAZON":
28+
case "ROKU":
29+
// These stores don't support subscription extensions
30+
console.log(`${store} does not support subscription extensions`);
31+
break;
32+
33+
default:
34+
console.log(`Unknown store: ${store}`);
35+
}
36+
}
37+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"event": {
3+
"type": "INITIAL_PURCHASE",
4+
"id": "12345678-1234-1234-1234-123456789012",
5+
"app_id": "app1234567890",
6+
"event_timestamp_ms": 1702819200000,
7+
"product_id": "com.yourapp.subscription.monthly",
8+
"period_type": "NORMAL",
9+
"purchased_at_ms": 1702819200000,
10+
"expiration_at_ms": 1705497600000,
11+
"environment": "PRODUCTION",
12+
"entitlement_ids": ["premium"],
13+
"presented_offering_id": "holiday_promo",
14+
"transaction_id": "2000000456789012",
15+
"original_transaction_id": "2000000456789012",
16+
"is_family_share": false,
17+
"country_code": "US",
18+
"app_user_id": "user_abc123",
19+
"aliases": ["$RCAnonymousID:8069238d6049ce87cc529853916d624c"],
20+
"original_app_user_id": "user_abc123",
21+
"currency": "USD",
22+
"price": 9.99,
23+
"price_in_purchased_currency": 9.99,
24+
"subscriber_attributes": {
25+
"$email": {
26+
"updated_at_ms": 1702819200000,
27+
"value": "customer@example.com"
28+
}
29+
},
30+
"store": "APP_STORE",
31+
"tax_percentage": 0.0,
32+
"commission_percentage": 0.3,
33+
"offer_code": "HOLIDAY2024"
34+
},
35+
"api_version": "1.0"
36+
}
37+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import express, { Request, Response } from "express";
2+
3+
const app = express();
4+
app.use(express.json());
5+
6+
const WEBHOOK_AUTH_HEADER = process.env.WEBHOOK_AUTH_HEADER!;
7+
8+
app.post("/webhooks/revenuecat", async (req: Request, res: Response) => {
9+
// Verify authorization header
10+
const authHeader = req.headers.authorization;
11+
if (authHeader !== WEBHOOK_AUTH_HEADER) {
12+
return res.status(401).json({ error: "Unauthorized" });
13+
}
14+
15+
// Respond immediately to avoid timeout
16+
res.status(200).json({ received: true });
17+
18+
// Process asynchronously
19+
processWebhook(req.body);
20+
});
21+

0 commit comments

Comments
 (0)