Skip to content

Commit 2382b46

Browse files
committed
docs: subscription offers
1 parent 4fd9256 commit 2382b46

File tree

1 file changed

+88
-3
lines changed

1 file changed

+88
-3
lines changed

docs/docs/guides/subscription-offers.md

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,9 @@ iOS supports promotional offers through the `withOffer` parameter. These are con
150150
interface DiscountOfferInputIOS {
151151
offerIdentifier: string; // From App Store Connect
152152
keyIdentifier: string; // From App Store Connect
153-
nonce: string; // UUID string
154-
signature: string; // From App Store Connect
155-
timestamp: number; // Unix timestamp
153+
nonce: string; // UUID v4 (lowercase)
154+
signature: string; // Base64-encoded signature from your server
155+
timestamp: number; // Unix timestamp in milliseconds
156156
}
157157

158158
const purchaseWithPromotionalOffer = async (
@@ -172,6 +172,91 @@ const purchaseWithPromotionalOffer = async (
172172
};
173173
```
174174

175+
##### Server-Side Signature Generation
176+
177+
Promotional offer signatures must be generated on your server and **must be base64-encoded**. Here's a complete example:
178+
179+
```javascript
180+
// Node.js server example
181+
const crypto = require('crypto');
182+
const {v4: uuidv4} = require('uuid');
183+
184+
function generatePromotionalOfferSignature(
185+
bundleId, // Your app's bundle identifier (e.g., "com.example.app")
186+
productId, // Product identifier (e.g., "premium_monthly")
187+
offerId, // Offer identifier from App Store Connect
188+
applicationUsername, // User identifier (appAccountToken)
189+
privateKey, // PKCS#8 PEM-formatted private key from App Store Connect
190+
keyId, // Key ID from App Store Connect
191+
) {
192+
// Generate nonce and timestamp
193+
const nonce = uuidv4().toLowerCase(); // MUST be lowercase UUID
194+
const timestamp = Date.now(); // Milliseconds since Unix epoch
195+
196+
// ⭐ CRITICAL: Data must be joined in this exact order
197+
const dataToSign = [
198+
bundleId, // 1. App Bundle ID
199+
keyId, // 2. Key Identifier
200+
productId, // 3. Product Identifier
201+
offerId, // 4. Offer Identifier
202+
applicationUsername, // 5. Application Username (appAccountToken)
203+
nonce, // 6. Nonce (lowercase UUID)
204+
timestamp.toString(), // 7. Timestamp (milliseconds)
205+
].join('\u2063'); // Join with invisible separator (U+2063)
206+
207+
// Sign the data with SHA-256
208+
const sign = crypto.createSign('sha256');
209+
sign.update(dataToSign);
210+
const signatureBuffer = sign.sign({
211+
key: privateKey,
212+
format: 'pem',
213+
type: 'pkcs8',
214+
});
215+
216+
// ⭐ CRITICAL: Signature MUST be base64-encoded
217+
const base64Signature = signatureBuffer.toString('base64');
218+
219+
return {
220+
identifier: offerId,
221+
keyIdentifier: keyId,
222+
nonce: nonce, // Lowercase UUID
223+
signature: base64Signature, // Base64-encoded signature
224+
timestamp: timestamp, // Milliseconds
225+
};
226+
}
227+
228+
// Usage example
229+
app.post('/generate-offer-signature', (req, res) => {
230+
const {productId, offerId, applicationUsername} = req.body;
231+
232+
const signature = generatePromotionalOfferSignature(
233+
process.env.APP_BUNDLE_ID, // From environment
234+
productId,
235+
offerId,
236+
applicationUsername,
237+
process.env.APPLE_PRIVATE_KEY, // PKCS#8 PEM private key
238+
process.env.APPLE_KEY_ID, // From App Store Connect
239+
);
240+
241+
res.json(signature);
242+
});
243+
```
244+
245+
**Important Notes:**
246+
247+
1. **Data Order**: The 7 fields MUST be joined in the exact order shown above (as per [Apple's documentation](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers))
248+
2. **Nonce**: Must be a lowercase UUID v4 string
249+
3. **Timestamp**: Must be in milliseconds (not seconds)
250+
4. **Signature**: Must be base64-encoded (not hex or raw)
251+
5. **Private Key**: Must be PKCS#8 PEM format from App Store Connect
252+
6. **Separator**: Use Unicode character U+2063 (invisible separator)
253+
254+
**Common Errors:**
255+
256+
- "The data couldn't be read because it isn't in the correct format" → Signature is not base64-encoded
257+
- "Invalid signature" → Incorrect data order or wrong separator
258+
- "Signature verification failed" → Wrong private key or key ID
259+
175260
## Common Patterns
176261

177262
### Selecting Specific Offers

0 commit comments

Comments
 (0)