ساخت فرایند پرداخت
پیش از شروع
- Abrova Sync، A/B Testing و Push (بهزودی)
- ابرووا تریس
A real checkout has more moving parts than any one SDK covers: the cart the user builds, the price they see, the confirmation after they pay, and the errors along the way. This guide wires all four. The products are independent, so you can ship the cart first and add the rest later. They share one Abrova project: one public key (ab_live_…) for every SDK on every platform, and a server secret (ab_srv_…) for your backend, both in Abrova Console under Project settings → Keys.
How the pieces fit together#
- 1Add to cart (Abrova Sync)The cart is one document per user. It follows the user across devices and keeps working offline.
- 2Checkout page (Abrova A/B Testing)The user gets a stable price variant; the purchase is counted as a conversion for it.
- 3Order confirmed (Abrova Push)Your backend sends the confirmation once the payment provider confirms the charge.
- 4Along the way (Abrova Trace)Errors you catch are reported with the user id, the step that failed and the price variant.
Initialize the SDKs at app start#
Do this once in your app entry point, with the same key for every SDK. Start Trace first so it catches errors from everything after it. fetchAbrovaToken is your own function; step 2 defines it.
Flutter
Packages: abrova_trace_flutter_sdk (from Abrova Center today), abrova_push, abrova_sync, abrova_ab_testing (when they launch).
import 'package:flutter/material.dart';
import 'package:abrova_trace_flutter_sdk/abrova_trace_flutter_sdk.dart';
import 'package:abrova_push/abrova_push.dart';
import 'package:abrova_sync/abrova_sync.dart';
import 'package:abrova_ab_testing/abrova_ab_testing.dart';
const abrovaApiKey = 'ab_live_your_api_key'; // one key for every SDK
void main() async {
// 1. Trace first.
await AbrovaTrace.initWithZone(
AbrovaTraceConfig(apiKey: abrovaApiKey, release: '1.0.0'),
() async {
WidgetsFlutterBinding.ensureInitialized();
// 2. Push: order confirmations.
await AbrovaPush.init(AbrovaPushConfig(apiKey: abrovaApiKey));
// 3. Sync: the cart. offlineEnabled keeps writes working without a network.
await AbrovaSync.init(AbrovaSyncConfig(
apiKey: abrovaApiKey,
offlineEnabled: true,
tokenProvider: fetchAbrovaToken, // null while signed out
));
await AbrovaSync.connect();
// 4. A/B Testing: the price test.
await AbrovaAbTesting.init(AbrovaAbTestingConfig(
apiKey: abrovaApiKey,
tokenProvider: fetchAbrovaToken,
));
runApp(const MyApp());
},
);
}iOS
import AbrovaTrace
import AbrovaPush
import AbrovaSync
import AbrovaAbTesting
// In your AppDelegate or @main App init:
let apiKey = "ab_live_your_api_key"
AbrovaTrace.shared.initialize(config: AbrovaTraceConfigBuilder(apiKey: apiKey)
.release(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)
.build())
AbrovaPush.shared.initialize(config: AbrovaPushConfig(apiKey: apiKey))
let sync = AbrovaSync.initialize(config: AbrovaSyncConfigBuilder(apiKey: apiKey)
.offlineEnabled(true)
.build())
sync.userTokens.provider = { try await MyBackend.abrovaToken() } // your code, step 2
Task { try? await sync.connect() }
AbrovaAbTesting.shared.initialize(config: AbrovaAbTestingConfig(
apiKey: apiKey,
tokenProvider: { completion in MyBackend.abrovaToken(completion) } // your code, step 2
), delegate: self)Android
import ir.abrova.abtesting.AbrovaAbTesting
import ir.abrova.abtesting.AbrovaAbTestingConfig
import ir.abrova.abtesting.AbrovaTokenProvider
import ir.abrova.push.sdk.AbrovaPush
import ir.abrova.push.sdk.AbrovaPushConfig
import ir.abrova.sync.sdk.AbrovaSync
import ir.abrova.sync.sdk.AbrovaSyncConfig
import ir.abrova.trace.sdk.AbrovaTrace
import ir.abrova.trace.sdk.AbrovaTraceConfig
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
val apiKey = "ab_live_your_api_key"
AbrovaTrace.init(this, AbrovaTraceConfig.Builder(apiKey).build())
AbrovaPush.init(this, AbrovaPushConfig(apiKey = apiKey))
AbrovaSync.initialize(this, AbrovaSyncConfig.builder(apiKey)
.offlineEnabled(true)
.userTokenProvider { MyBackend.abrovaToken() } // your code, step 2; called off the main thread
.build())
AbrovaAbTesting.init(this, AbrovaAbTestingConfig(
apiKey = apiKey,
tokenProvider = AbrovaTokenProvider { MyBackend.abrovaTokenBlocking() },
))
}
}
// Connect Sync when ready (in an Activity or Fragment):
lifecycleScope.launch { AbrovaSync.getInstance().connect() }Identify the signed-in user#
The public key ships inside your app, so it can't prove who the user is. Sync, A/B Testing and Push take the user from an Abrova user token that your backend mints with the server secret. It is one token for all of them; it lasts an hour by default (expiresIn in seconds, 24 hours at most), and each SDK asks the provider again before it expires.
AbrovaAuth.instance.tokenProvider as tokenProvider and use the Auth user id wherever an SDK asks for a user id.Your backend: a token endpoint
// Behind your own login check. Never ship the server secret in an app.
app.post('/abrova-token', requireLogin, async (req, res) => {
const r = await fetch('https://auth.abrova.ir/users/token', {
method: 'POST',
headers: {
'x-api-key': process.env.ABROVA_API_KEY!,
'x-server-secret': process.env.ABROVA_SERVER_SECRET!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId: req.user.id }),
});
const { token } = await r.json();
res.json({ token });
});Flutter: the provider and sign-in
import 'dart:convert';
import 'package:http/http.dart' as http;
// Your code: calls the endpoint above with your app's own session.
Future<String?> fetchAbrovaToken() async {
if (yourSessionToken == null) return null; // nobody signed in
final res = await http.post(
Uri.parse('https://your-api.example.com/abrova-token'),
headers: {'Authorization': 'Bearer $yourSessionToken'},
);
return jsonDecode(res.body)['token'] as String;
}
Future<void> onSignedIn(String userId) async {
AbrovaTrace.setUserId(userId); // errors carry the user id
await AbrovaPush.register(userId: userId); // your backend can push to this user
await AbrovaAbTesting.instance.setUserId(userId); // the id your server puts in the token
await AbrovaAbTesting.instance.refreshUserToken(); // ask the provider again now
await AbrovaSync.refreshUserToken(); // reconnect Sync as this user
}iOS
AbrovaTrace.shared.setUserId(userId) AbrovaPush.shared.register(userId: userId) AbrovaAbTesting.shared.setUserId(userId) sync.refreshUserToken() // asks the provider again and reconnects as this user
Android
AbrovaTrace.setUserId(userId) AbrovaPush.register(userId = userId) AbrovaAbTesting.setUserId(userId) AbrovaAbTesting.refreshUserToken() AbrovaSync.getInstance().userTokens.refresh() // asks the provider again and reconnects as this user
Keep the cart in Abrova Sync#
Create a database named shop in Abrova Console (database names are unique within a project), then address it and its collections by name. Store each cart as one document whose id is the user id. The rest of this guide shows Flutter; the other Sync SDKs use the same database → collection → document model (see CRUD operations).
final carts = AbrovaSync.database('shop').collection('carts');
// Add to cart or change a quantity: replace the cart document.
await carts.set(userId, {
'items': [
{'sku': 'SKU-001', 'qty': 1, 'priceToman': 450000},
{'sku': 'SKU-042', 'qty': 2, 'priceToman': 120000},
],
'updatedAt': DateTime.now().toIso8601String(),
});
AbrovaTrace.addUserBreadcrumb('Cart updated', data: {'items': 2});
// Live updates: the same cart edited on another device shows up here.
final cartListener = carts.listenDocument(userId, (doc) {
if (doc != null) updateCartUI(doc.data); // your code
});
// When the cart screen closes:
cartListener.remove();With offlineEnabled: true, writes made without a network are saved on the device and sent when the connection comes back. If the same cart changed on the server in the meantime, the conflictStrategy config option decides which version wins (serverWins by default, or clientWins, merge, manual); see Conflict resolution.
To stop one user reading another's cart, add a security rule on carts that compares the document id with auth.uid, which comes from the user token in step 2:
{
"rules": {
"carts": {
"$userId": {
".read": "auth.uid === $userId",
".write": "auth.uid === $userId"
}
}
}
}A rule on the document id suits reading one cart (listenDocument), not a query over all carts, which is all this page needs.
Show the A/B price#
In Abrova Console → A/B Testing, create an experiment with the key checkout-price-test and give each variant a configuration, for example {"discountPercent": 0} for control and {"discountPercent": 10} for the test variant.
When the checkout page opens, ask for the user's variant. Assignment is sticky: once a user is assigned, the SDK caches it and returns the same variant every time.
const priceTest = 'checkout-price-test';
final variant = await AbrovaAbTesting.instance.getVariant(priceTest);
final config = await AbrovaAbTesting.instance.getVariantConfig(priceTest);
final discount = int.tryParse('${config?['discountPercent'] ?? 0}') ?? 0;
final payToman = cartTotalToman * (100 - discount) ~/ 100;
// Every Trace error from now on carries the variant in its extra data.
AbrovaTrace.setExtra('priceVariant', variant);getVariant never throws. Before the user and their token are set (step 2) it returns the default variant, so run this after sign-in to get the real one.
Record the purchase and send the confirmation push#
When payment succeeds, the app records a conversion for the experiment. The value is what A/B Testing adds up as revenue per variant:
await AbrovaAbTesting.instance.trackConversion( priceTest, value: payToman.toDouble(), );
Your backend sends the confirmation once your payment provider confirms the charge. sendToUser reaches every device registered with that user id (step 2). Package: @abrova/push-node.
import { AbrovaPush } from '@abrova/push-node';
const abrovaPush = new AbrovaPush({
apiKey: process.env.ABROVA_API_KEY!,
serverSecret: process.env.ABROVA_SERVER_SECRET!, // server only
});
const result = await abrovaPush.push.sendToUser({
userId: order.userId,
title: 'Order confirmed',
body: `Your order of ${order.itemCount} items is on the way.`,
data: { orderId: order.id },
});
if (result.reason === 'no_recipients') {
// This user has no registered device: fall back to e-mail or SMS.
}A successful send means the notification was accepted for delivery, not that a device received it. Use result.messageId with delivery receipts if you need to know.
On the client, open the order when the user taps the notification:
// Taps while the app is running (foreground or background).
AbrovaPush.onNotificationTapped((message) {
final orderId = message.data?['orderId'];
if (orderId != null) navigatorKey.currentState?.pushNamed('/orders/$orderId');
});
// A tap that launched the app arrives here instead, once your routing is ready.
final initial = await AbrovaPush.getInitialMessage();
final initialOrderId = initial?.data?['orderId'];
if (initialOrderId != null) navigatorKey.currentState?.pushNamed('/orders/$initialOrderId');Capture checkout errors with Trace#
This step works today. Trace reports uncaught errors on its own. For errors you catch yourself, such as a failed payment call, report them with captureException. The report carries the user id from step 2, the priceVariant from step 4, the breadcrumbs you added, and the extra data you pass here.
try {
await processPayment(cart, payToman); // your code
await AbrovaAbTesting.instance.trackConversion(priceTest, value: payToman.toDouble());
navigateToConfirmation(); // your code
} catch (err, stack) {
await AbrovaTrace.captureException(
err,
stackTrace: stack,
extra: {
'checkoutStep': 'payment',
'cartItemCount': cart.items.length,
},
);
showErrorBanner('We could not process your payment. Try again in a moment.'); // your code
}To check it worked, open Abrova Console → Trace → Issues and open the error. The Context card shows the extra data (user_id, priceVariant, checkoutStep, cartItemCount), and the same page has the stack trace and the breadcrumbs, including Cart updated. Repeats of the same error are grouped into one issue, which keeps the extra data of the latest occurrence.
Read the A/B test results#
Once enough users have checked out, open Abrova Console → A/B Testing → checkout-price-test. For each variant you get participants, conversion rate, revenue from the values you tracked, and whether the difference to control is statistically significant.
A/B Testing and Trace are separate pages. To check that a winning price didn't also break checkout, look at the payment errors in Trace and the priceVariant each one carries.
Going further#
- Pick the conflict strategy for a cart edited on two devices at once: Conflict resolution.
- Keep the confirmation text in a push template (
templateIdonsendToUser) so it can change without a deploy: Templates. - Drop expected errors such as "card declined" before they become issues: Ignore rules.
- سبد را با قانون امنیتی روی auth.uid محافظت کنید.
بعدی: Abrova Sync