דלג לתוכן הראשי

שילוב React Native SDK

עטיפת React Native שמגשרת בין ערכות ה-SDK הנייטיביות של iOS ו-Android, ומספקת API יחיד ב-TypeScript עם כל יכולות הפלטפורמה.

מינימום iOS 14.0

ה-SDK הנייטיבי דורש iOS 14.0, בעוד ש-React Native 0.75 עדיין מגדיר כברירת מחדל 13.4. CocoaPods דוחה את אי-ההתאמה עם שגיאה מטעה - "Specs satisfying the dependency were found, but they required a higher minimum deployment target" - שנשמעת כמו בעיה ביישוב גרסאות ולא כמו בעיית פלטפורמה.

העלו את המינימום ב-ios/Podfile:

platform :ios, [min_ios_version_supported, '14.0'].max

לקיחת הגבוה מבין השניים ממשיכה לעבוד גם כשגרסה עתידית של React Native תעלה את המינימום שלה מעל שלנו.

לאחר מכן העלו גם את יעד הפריסה של האפליקציה עצמה ל-14.0 או יותר: ב-Xcode בחרו את ה-target של האפליקציה, ואז General → Minimum Deployments → iOS (הגדרת הבנייה IPHONEOS_DEPLOYMENT_TARGET ב-ios/<YourApp>.xcodeproj). אפליקציית React Native 0.75 חדשה נשארת שם על 13.4 גם אחרי השינוי ב-Podfile, וה-linker מתריע אז שקובצי Joryio נבנו לגרסת iOS חדשה יותר מזו של האפליקציה.

יכולות​

  • גשר נייטיבי - עוטף את ערכות ה-SDK הנייטיביות של iOS (Swift) ו-Android (Kotlin)
  • מעקב אירועים - מעקב אירועים מותאמים וצפיות במסך
  • זיהוי משתמשים - זיהוי, כינויים (aliases) עם תווית וניהול מאפייני משתמש
  • התראות פוש - רישום טוקנים, מעקב אחר לחיצות (FCM + APNs)
  • הודעות In-App - קבלה והצגת הודעות בתוך האפליקציה
  • מסחר אלקטרוני - מעקב רכישות, עגלה, תשלום
  • תמיכה במצב לא מקוון - אירועים נשמרים מקומית ומסונכרנים כשיש חיבור
  • ניסיונות חוזרים - השהיה מעריכית בין ניסיונות לאחר כשל ברשת

דרישות​

  • React Native 0.75+. ב-iOS ה-SDK הנייטיבי מקושר כחבילת Swift דרך spm_dependency של React Native, שנוסף בגרסה 0.75. ב-Expo - SDK 52 ומעלה.
  • הארכיטקטורה הישנה והחדשה. בארכיטקטורה החדשה המודול רץ דרך שכבת התאימות (interop layer) של React Native למודולים ותיקים, שפועלת כברירת מחדל.
  • iOS 14.0+ / Android API 23+ (Android 6.0)
  • TypeScript 5.0+ (מומלץ)

התקנה​

npm install @joryio/react-native-sdk

הגדרת iOS​

cd ios && pod install

pod install מוסיף את Joryio iOS SDK לפרויקט ה-Pods כחבילת Swift (https://github.com/joryio/joryio-ios-sdk, ה-products Joryio ו-JoryioUI, בדיוק בגרסה של חבילה זו), ו-Xcode מוריד אותה בפעם הראשונה שהוא בונה את האפליקציה - ולכן הבנייה הזו צריכה גישת רשת ל-github.com. שום דבר לא מגיע מ-CocoaPods trunk, שמפסיק לקבל גרסאות חדשות ב-2 בדצמבר 2026.

React Native מדפיס במהלך pod install שתי שורות [SPM] WARNING לכל ספרייה שמשתמשת בחבילות Swift: אחת מבקשת לפתוח מחדש את ה-workspace אם אתם עובדים עם Xcode 15 או ישן יותר, והשנייה מציינת שהחבילה מקושרת סטטית. אם האפליקציה לא מצליחה לעבור קישור (link), התקינו עם frameworks דינמיים: USE_FRAMEWORKS=dynamic pod install.

שדרוג מגרסה קודמת

גרסאות קודמות קיבלו את ה-SDK של iOS מ-CocoaPods (ה-pod Joryio/UI). אם הוספתם בעצמכם שורת pod 'Joryio...' ל-Podfile - למשל pod עם :path שמצביע על עותק מקומי - מחקו אותה והריצו pod install שוב.

הגדרת Android​

אין מה להוסיף. החבילה מקושרת אוטומטית (autolinking - היא כוללת react-native.config.js), כך ש-PackageList כבר רושם את JoryioPackage - אל תוסיפו אותו גם ידנית ב-MainApplication, כי זה רושם את המודול פעמיים. הגשר תלוי ב-io.joryio:joryio-android-ui מ-Maven Central, באותה גרסה כמו החבילה.

Expo​

ה-SDK הוא מודול Native, ולכן הוא לא רץ ב-Expo Go - השתמשו ב-development build (npx expo prebuild או EAS Build) ב-Expo SDK 52 ומעלה. ה-deployment target של iOS שמוגדר כברירת מחדל ב-Expo יכול להיות נמוך מ-14.0 שה-SDK צריך; העלו אותו עם expo-build-properties בקובץ app.json:

{
"expo": {
"plugins": [["expo-build-properties", { "ios": { "deploymentTarget": "14.0" } }]]
}
}

התחלה מהירה​

import { Platform } from 'react-native';
import Joryio from '@joryio/react-native-sdk';

// אתחול פעם אחת ב-App.tsx
await Joryio.initialize(
Platform.select({
ios: 'jry_sdk_ios_YOUR_IOS_KEY', // מפתח iOS מלוח הבקרה
android: 'jry_sdk_android_YOUR_ANDROID_KEY', // מפתח Android מלוח הבקרה
})!,
'api-eu1.joryio.com', // מארח ה-API (שם מארח בלבד, ללא סכמה)
{
enableDebug: __DEV__,
trackSessionStart: true,
}
);

לכל פלטפורמה יש מפתח SDK משלה: מפתחות iOS מתחילים ב-jry_sdk_ios_ ומפתחות Android ב-jry_sdk_android_. מפתח שהפלטפורמה דוחה גורם ל-initialize() לרשום אזהרה ומשאיר את ה-SDK כבוי, עד שתקראו לו שוב עם המפתח הנכון.

מעקב אירועים​

מעקב אירועים מותאמים​

// אירוע בסיסי
Joryio.track('Button Clicked');

// אירוע עם מאפיינים
Joryio.track('Product Added', {
productId: 'SKU-123',
productName: 'חולצה כחולה',
price: 29.99,
currency: 'ILS',
});

// מסחר אלקטרוני: השתמשו ב-Joryio.ecommerce (ראו "מסחר אלקטרוני" בהמשך) - הוא שולח את השמות והמפתחות שדוחות ההכנסות קוראים

מעקב צפיות במסך​

Joryio.trackScreen('ProductDetail', { productId: 'SKU-123' });
Joryio.trackScreen('Cart');
Joryio.trackScreen('Checkout');

שילוב עם React Navigation​

import { useRef } from 'react';
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';

function App() {
const navigationRef = useNavigationContainerRef();
const routeNameRef = useRef<string>();

return (
<NavigationContainer
ref={navigationRef}
onStateChange={() => {
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
if (currentRouteName && currentRouteName !== routeNameRef.current) {
Joryio.trackScreen(currentRouteName);
routeNameRef.current = currentRouteName;
}
}}
>
{/* ... */}
</NavigationContainer>
);
}

זיהוי משתמשים​

זיהוי לאחר התחברות​

שלחו מספרי טלפון עם + וקידומת מדינה

שלחו תמיד מספרי טלפון בפורמט בינלאומי מלא (E.164): +, קידומת המדינה ואז המספר - למשל +972501234567. מספר בלי + נקרא לפי מדינת ברירת המחדל למספרי טלפון של סביבת העבודה; אם לא הוגדרה, או שאפשר לקרוא את הספרות בשתי דרכים, הוא נשמר כלא מאומת ולא משמש להודעות. איך מטופלים מספרי טלפון

// לאחר login
Joryio.identify('user-123');

// עם מאפיינים
Joryio.identify('user-123');
Joryio.setAttributes({
firstName: 'John',
plan: 'premium',
});

כינויים (Aliases)​

צירוף מזהה ממערכת אחרת - מזהה CRM, מספר מועדון - לאדם הנוכחי. זה עובד גם עבור מבקרים אנונימיים: הכינוי נשמר מול המזהה האנונימי ונשאר עם האדם גם אחרי שהוא מזוהה.

Joryio.addAlias('crm_id', 'C-1042');

אין alias(userId) ללא תווית; כדי לתת שם לאדם הנוכחי קראו ל-identify(userId).

איפוס (התנתקות)​

ניקוי זהות המשתמש והתחלת סשן אנונימי חדש:

Joryio.reset();

מאפייני משתמש​

// הגדרת מספר מאפיינים
Joryio.setAttributes({
firstName: 'ישראל',
lastName: 'ישראלי',
plan: 'premium',
age: 28,
isVIP: true,
});

// הגדרת מאפיין יחיד
Joryio.setAttribute('favoriteColor', 'blue');

// הגדלת ערך מספרי
Joryio.incrementAttribute('loginCount', 1);
Joryio.incrementAttribute('totalSpent', 29.99);

// הסרת מאפיין
Joryio.unsetAttribute('temporaryFlag');

ערך null מסיר את המאפיין בשתי הפלטפורמות: setAttribute('nickname', null) ו-setAttributes({ nickname: null }) שקולים ל-unsetAttribute('nickname').

מסחר אלקטרוני​

עקבו אחרי רכישות ואחרי השלבים שלפניהן עם Joryio.ecommerce. הוא שולח את שמות האירועים ומפתחות המאפיינים הסטנדרטיים שדוחות ההכנסות של Joryio קוראים, כך שאין צורך להקליד אף אחד מהם ידנית:

Joryio.ecommerce.viewProduct({ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99 });
Joryio.ecommerce.addToCart({ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99, quantity: 2 });
Joryio.ecommerce.startCheckout(
[{ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99, quantity: 2 }],
59.98, // ערך העגלה
);

// נשלח כ-"Order Completed" עם orderId ו-total - המפתחות שדוחות ההכנסות קוראים
Joryio.ecommerce.purchase({
orderId: 'ORD-789',
items: [{ productId: 'SKU-123', name: 'Blue T-Shirt', price: 29.99, quantity: 2 }],
value: 59.98,
currency: 'USD',
});

purchase שולח אירוע Order Completed עם orderId ו-total (ה-value של ההזמנה) - שני המפתחות שדוחות ההכנסות והייחוס קוראים. קריאה ידנית כמו track('Order Completed', { order_id, value }) נשמרת כפי שנכתבה ונספרת כהזמנה של 0 ללא מזהה הזמנה: בכוונה אין שכבת כינויים שממפה כתיבים אחרים. אם אתם עוקבים אחרי רכישה בעצמכם, השתמשו ב-orderId וב-total.

שאר הקריאות: viewCategory(categoryId, categoryName), search(query, resultCount?), removeFromCart(product, quantity?), cartViewed(items, cartValue), updateCart(items, cartValue), addPaymentInfo(paymentMethod), orderFulfilled(orderId, trackingNumber?, carrier?), orderDelivered(orderId), orderCancelled(orderId, reason?), orderRefunded(orderId, refundAmount?, reason?), addToWishlist(product), removeFromWishlist(product), shareProduct(product, shareMethod), applyCoupon(couponCode, discountAmount?, discountType?), removeCoupon(couponCode), submitReview(productId, rating, reviewText?). ecommerce זמין גם כייצוא בשם: import { ecommerce } from '@joryio/react-native-sdk'.

קריאה שחסר בה ארגומנט חובה - purchase בלי orderId, items או value מספרי, מוצר בלי productId, עגלה בלי items - רושמת אזהרה ולא שולחת דבר, כך שהיא לעולם לא נרשמת כהזמנה בסכום 0.

התראות פוש​

מתי לבקש

בקשו את ההרשאה כשלמשתמש יש סיבה להסכים. בקשה בשניות הראשונות של הסשן היא זו שנדחית הכי הרבה, וסירוב הרבה יותר קשה לשינוי מאשר עיכוב - בקשת המערכת היא חד-פעמית, ולכן "לא" שם הוא למעשה קבוע, בעוד שבקשה מאוחרת יותר לא עולה לכם דבר.

ההנחיה נכונה לא משנה איזה מנגנון מבקש. אם Firebase Messaging הוא שמציג את הבקשה, כמו בדוגמה למטה, התזמון בידיים שלכם; אם ה-SDK של Joryio מציג אותה, הוא לא יבקש במקומכם אלא אם הגדרתם requestPushPermissionAtLaunch, שכבויה כברירת מחדל ולא חסרה.

הגדרה עם Firebase​

import { Platform } from 'react-native';
import messaging from '@react-native-firebase/messaging';

// בקשת הרשאה
const authStatus = await messaging().requestPermission();

// קבלת ורישום טוקן
// Joryio שולחת פוש ל-iOS דרך APNs, ולכן ב-iOS רשמו את טוקן ה-APNs, לא את טוקן ה-FCM.
const token = Platform.OS === 'ios'
? await messaging().getAPNSToken()
: await messaging().getToken();
if (token) {
const result = await Joryio.registerPushToken(token);
if (!result.success && result.isRetryable) {
// תקלה זמנית (אופליין, DNS, 5xx): רשמו שוב כשהרשת חוזרת
}
}

// מעקב אחר רענון טוקן
messaging().onTokenRefresh((newToken) => {
// onTokenRefresh מוסר את טוקן ה-FCM - גם ב-iOS, בכל הפעלה של האפליקציה.
// רשמו אותו באנדרואיד בלבד; ב-iOS הטוקן הנכון הוא טוקן ה-APNs שלמעלה.
if (Platform.OS === 'android') Joryio.registerPushToken(newToken);
});

ב-iOS רשמו רק את טוקן המכשיר של APNs. onTokenRefresh של React Native Firebase מוסר גם ב-iOS את טוקן ה-FCM, בכל הפעלה של האפליקציה, ורישום שלו היה מחליף את טוקן ה-APNs התקין של המכשיר בטוקן ש-APNs דוחה - לכן רשמו אותו באנדרואיד בלבד, כמו בדוגמה. ב-iOS, registerPushToken דוחה טוקן שאינו טוקן APNs (הקסדצימלי) ומחזיר success: false ו-isRetryable: false.

אם המשתמש כיבה את הפוש וקראתם ל-unregisterPush(), אל תרשמו טוקנים מתוך callbacks אוטומטיים (onTokenRefresh, getToken() בעליית האפליקציה) עד שיפעיל אותו מחדש: registerPushToken() הוא ה"פוש פועל" המפורש והוא מבטל את ה-unregister. בדקו את הגדרת הפוש שלכם לפני הרישום.

registerPushToken מחזיר Promise עם התוצאה (success, message, isUnauthorized, isRetryable). הוא לעולם לא נדחה, כך שהתעלמות מה-Promise שומרת על ההתנהגות הקודמת - אבל תקלה זמנית משאירה את המכשיר לא רשום עד ההפעלה הבאה, לכן בדקו את isRetryable ורשמו שוב. getDiagnostics().lastError נושא את אותו דגל isRetryable.

טיפול בלחיצה על התראה​

import messaging from '@react-native-firebase/messaging';

// כשהאפליקציה ברקע והמשתמש לוחץ על ההתראה
messaging().onNotificationOpenedApp((remoteMessage) => {
const trackingId = remoteMessage.data?.trackingId;
if (trackingId) {
Joryio.trackPushClick(trackingId);
}
});

// כשהאפליקציה הייתה סגורה ונפתחה דרך ההתראה
messaging()
.getInitialNotification()
.then((remoteMessage) => {
if (remoteMessage?.data?.trackingId) {
Joryio.trackPushClick(remoteMessage.data.trackingId);
}
});

אישורי מסירה​

כשספריית ההודעות שלכם מקבלת את הפוש (React Native Firebase, Expo notifications), ה-SDK הנייטיבי לא רואה אותו, לכן דווחו על המסירה בעצמכם עם ה-trackingId מתוך ה-data של ההודעה:

messaging().onMessage(async (msg) => {
if (msg.data?.trackingId) Joryio.reportPushDelivered(String(msg.data.trackingId));
});
messaging().setBackgroundMessageHandler(async (msg) => {
if (msg.data?.trackingId) Joryio.reportPushDelivered(String(msg.data.trackingId));
});

ביטול רישום​

Joryio.unregisterPush();

unregisterPush() מפסיק את שליחת הפושים למכשיר הזה. הביטול נשמר גם אחרי הפעלה מחדש, עד שתקראו שוב ל-registerPushToken().

בדיקת סטטוס פוש​

const enabled = await Joryio.isPushEnabled();
console.log('Push enabled:', enabled);

בקשת הרשאה​

if ((await Joryio.getPushPermissionStatus()) === 'notDetermined') {
// הציגו קודם הסבר משלכם, ואז:
const status = await Joryio.requestPushPermission(); // 'granted' | 'denied' | 'notDetermined' | 'unavailable'
}

requestPushPermission() מתנהג אותו דבר ב-iOS ובאנדרואיד:

  • הוא תמיד מסתיים. באנדרואיד, אם ה-Activity נוצר מחדש בזמן שחלון ההרשאה מוצג (סיבוב מסך, החלפת ערכת נושא), הוא מחזיר את הסטטוס הנוכחי כשהאפליקציה חוזרת לחזית.
  • כשהמשתמש ביטל את ההסכמה למעקב, חלון ההרשאה עדיין מוצג והתוצאה מוחזרת - ההחלטה אם האפליקציה רשאית לשלוח התראות היא של המשתמש במערכת ההפעלה - אבל דבר לא נרשם או מדווח ל-Joryio כל עוד ההסכמה מבוטלת.

הודעות In-App​

ההודעות מוצגות מעצמן. התקינו את ה־SDK, שלחו קמפיין, והוא יופיע - מצויר על ידי ה־View־ים הטבעיים, עם הגופנים, הצבעים והמצב הכהה של האפליקציה שלכם. אין מה לחבר.

שאר הסעיף הזה נחוץ רק אם ברצונכם להציג את ההודעות ב־React במקום - ראו השתלטות על הרינדור.

אסימוני שליחה (delivery tokens)​

כאשר השרת מגיש קמפיין מתאים, הוא מנפיק עבורו גם אסימון שליחה חתום וקצר-מועד. ה-SDK מחזיר את האסימון כשהוא מדווח על הצגה, קליק או סגירה, והשרת מאמת את החתימה לפני שהוא רושם משהו.

אינך צריך לעשות דבר - ה-SDK מטפל בזה עבורך. התיעוד כאן קיים כי ניתן להגדיר את השרת לדרוש את האסימון. כברירת מחדל דיווח ללא אסימון עדיין מתקבל - השרת רושם אותו ביומן כלקוח מגרסה שקדמה לאסימונים - ורק כאשר השרת רץ עם IN_APP_REQUIRE_DELIVERY_TOKEN=true אותו דיווח נדחה:

POST /v1/in-app/track (no deliveryToken)
{ "success": false, "error": "A delivery token is required" }

אסימון שנשלח אך אינו עובר אימות - מזויף, שונה או שפג תוקפו - נדחה תמיד, ללא קשר להגדרה.

האסימון הוא מה שהופך דיווח הצגה לאמין: בלעדיו, כל מי שמחזיק במפתח ה-SDK - שנשלח בתוך כל אפליקציה ובכל עמוד - היה יכול לדווח על הצגות וקליקים לקמפיין שמעולם לא הוצג, והדוחות שלך היו סופרים אותם.

אם הצגות in-app מפסיקות להירשם מול שרת שאוכף אסימונים, ודא שהאפליקציה נבנתה מול גרסה עדכנית של ה-SDK: בנייה שנעשתה לפני שאסימוני השליחה נוספו אינה שולחת אסימון.

שני סוגי תוכן​

לכל הודעה יש שדה kind שמציין את צורת התוכן. פצלו לפיו:

kindמה הוא נושאאיך להציג
'native'title, body, imageUrl, buttonsרכיבי React Native - <Text>, <Image>, <Pressable>
'html'html, cssWebView

השתלטות על הרינדור​

הרשמה עם onInAppMessage מפסיקה את הציור של ה־SDK ומעבירה כל הודעה לקוד שלכם, כדי שתציגו אותה עם רכיבי React Native. לא תקבלו שני עותקים.

הצהירו מה הרינדור שלכם יודע לצייר:

Joryio.onInAppMessage(handleMessage, {
capabilities: ['content.native'], // הוסיפו 'content.html' רק אם יש לכם WebView
});

קמפיינים מטורגטים לפי מה שהמכשיר מדווח, ויכולת שלא הוצהרה נקראת כ"לא יכול" ולא כ"לא ידוע". השמטת האפשרות שומרת על ההתנהגות הקודמת, שבה הערך הגיע מדגל הגדרה נייטיבי שמתאר את התצוגות של ה-SDK הנייטיבי עצמו - שלא רצות בכלל ברגע שנרשמתם.

עשו זאת אם אתם רוצים שההודעות יתאימו לשאר הממשק שלכם. הגשר עצמו תלוי ברכיבי הממשק הנייטיביים (io.joryio:joryio-android-ui באנדרואיד, ה-product JoryioUI של חבילת ה-Swift ב-iOS), ולכן הם נשארים מקושרים; ההרשמה רק מפסיקה את הציור שלהם.

הסרת ההרשמה האחרונה מחזירה את הרינדור ל-SDK הנייטיבי, כך שהודעות שמגיעות אחר כך מוצגות באופן נייטיבי ולא הולכות לאיבוד. רשימת capabilities ריקה שקולה להשמטתה (לא הוצהר) בשתי הפלטפורמות.

פעולות הכפתורים הן dismiss, url, deep_link ו-request_push_permission - ה"כן" של הסבר מקדים לפוש: סגרו את ההודעה וקראו ל-Joryio.requestPushPermission().

מתי להירשם​

החל מגרסה 1.3.2. הירשמו לפני ש-initialize() הסתיים - ברמת המודול, או ב-effect שרץ בעליית האפליקציה - ואף הודעה לא תצויר על ידי ה-SDK הנייטיב, גם לא הראשונה: ה-SDK מחזיק את הודעות ה-In-App עד ש-initialize() הסתיים וההרשמה שלכם הגיעה ל-SDK הנייטיב, מעביר את ההודעה המוחזקת ל-listener שלכם, ומחזיר את הרינדור ל-SDK הנייטיב רק אם אף אחד לא נרשם.

אם אתם נרשמים מאוחר יותר - במסך שעולה אחרי ש-initialize() הסתיים - העבירו holdInAppMessagesForHandler: true ל-initialize(). הודעות שמוכנות לפני ההרשמה מוחזקות אז (לא מצוירות, לא מדווחות, ולא נספרות במגבלת התדירות) ומועברות ל-listener שלכם כשהוא נרשם. קריאה ל-disableInAppMessages() במקום זאת מסיימת את ההחזקה, וה-SDK הנייטיב מצייר את ההודעה המוחזקת.

באנדרואיד הגשר מעביר ל-SDK הנייטיבי את ה-Activity הנוכחי ב-initialize(), כך שהודעות In-App יכולות להופיע כבר בסשן הראשון.

האזנה להודעות​

import { useEffect, useState } from 'react';
import Joryio, { type InAppMessage } from '@joryio/react-native-sdk';

function App() {
const [message, setMessage] = useState<InAppMessage | null>(null);

useEffect(() => Joryio.onInAppMessage(setMessage), []);

return <>{message && <InAppMessageHost message={message} onClose={() => setMessage(null)} />}</>;
}

הצגת הודעת Native​

תוכן Native הוא טקסט, לא markup. הכניסו אותו ל־<Text> - העברתו ל־WebView או ל־dangerouslySetInnerHTML תחזיר בדיוק את סיכון ההזרקה ש־Native נועד למנוע.

function InAppMessageHost({ message, onClose }: { message: InAppMessage; onClose: () => void }) {
// דווחו 'displayed' פעם אחת, כשההודעה מופיעה.
useEffect(() => {
Joryio.trackInAppImpression(message.id, 'displayed');
}, [message.id]);

// דווחו 'dismissed' בכל פעם שההודעה יורדת מהמסך - גם אחרי קליק.
const close = () => {
Joryio.trackInAppImpression(message.id, 'dismissed');
onClose();
};

if (message.kind === 'native') {
return (
<View>
{message.imageUrl && <Image source={{ uri: message.imageUrl }} />}
{message.title && <Text style={styles.title}>{message.title}</Text>}
<Text>{message.body}</Text>

{message.buttons.map((button) => (
<Pressable
key={button.id}
onPress={() => {
Joryio.trackInAppImpression(message.id, 'clicked', button.id);
if (button.action === 'url' && button.url) Linking.openURL(button.url);
if (button.action === 'request_push_permission') void Joryio.requestPushPermission();
close();
}}
>
<Text>{button.text}</Text>
</Pressable>
))}
</View>
);
}

// בריחה של `</` כדי שה-CSS של הקמפיין לא יוכל לסגור את תגית ה-<style>.
const css = message.css.replace(/<\//g, '<\\/');
return <WebView source={{ html: `<style>${css}</style>${message.html}` }} />;
}

החלת ה-Style של הקמפיין​

השדה message.style נושא את הדריסות שמחבר הקמפיין הגדיר. כל שדה אופציונלי, ושדה חסר משמעו ירושה מהמראה של האפליקציה שלכם - לכן החילו רק את מה שקיים, ואל תחליפו בברירות מחדל משלכם:

const st = message.style;
const text = {
// 'auto' מיישר לפי שפת ההודעה, לא לפי שפת המכשיר -
// I18nManager.isRTL הוא הבדיקה הלא נכונה כשאותו workspace שולח עברית ואנגלית
// לאותה אפליקציה.
writingDirection: 'auto' as const,
textAlign: st?.textAlign === 'center' ? 'center' : 'auto',
...(st?.fontFamily ? { fontFamily: st.fontFamily } : {}),
};

החילו fontFamily רק אם האפליקציה שלכם ארזה את הגופן; עדיף להישאר עם הגופן שלכם מאשר להחליף בגופן אקראי. אם הקמפיין הגדיר צבע כפתור בלי צבע טקסט, בחרו שחור או לבן לפי הניגודיות מול הצבע - ברירת מחדל לבנה הופכת צבע מותג בהיר לבלתי נראה.

הפעלת הודעות HTML​

הודעות HTML כבויות כברירת מחדל. ה־SDK הנייטיבי מציג הודעת HTML ב־WebView בתוך האפליקציה שלכם. סקריפטים שנכתבו בהודעה לא רצים שם - רק הגשר של ה־SDK רץ, והוא מטפל בכפתורים ובטפסים - אבל הפעלת WebView היא עדיין החלטה של צוות האפליקציה - לא משהו שמפעילים ממערכת השיווק:

await Joryio.initialize(sdkKey, apiHost, {
allowHtmlJsInAppMessages: true, // ברירת מחדל: false
});

השארת ההגדרה כבויה אינה מבטלת הודעות In-App - הודעות Native ימשיכו להגיע. רק קמפייני HTML מדולגים.

זה נכון כל עוד ה־SDK הוא זה שמצייר. ברגע שתקראו ל־enableInAppMessages(['content.native', 'content.html']), קוד ה־JS שלכם מצייר כל הודעה וההגדרה הזו כבר אינה חוסמת HTML - מה שהצהרתם בקריאה הזו הוא ההסכמה.

מעקב אחר חשיפות​

// כשהודעה מוצגת
Joryio.trackInAppImpression(message.id, 'displayed');

// כשהמשתמש לוחץ
Joryio.trackInAppImpression(message.id, 'clicked', button.id);

// כשהמשתמש סוגר
Joryio.trackInAppImpression(message.id, 'dismissed');

דווחו 'displayed' פעם אחת, כשההודעה מופיעה. הדיווח הזה הוא שרושם את החשיפה והוא שנספר במגבלות התדירות של הקמפיין - העברת ההודעה לקוד שלכם לא עושה אף אחד מהם, וה-SDK לא מדווח במקומכם. בקליק, העבירו את מזהה הכפתור או הקישור כארגומנט השלישי, elementId, כדי שהדוחות יוכלו לומר איזה כפתור נלחץ; השמיטו אותו ב-displayed וב-dismissed.

דווחו 'dismissed' בכל פעם שההודעה יורדת מהמסך, כולל אחרי קליק. עד אז ההודעה תופסת את משבצת התצוגה: ב-iOS לא מוצגת שום הודעת In-App אחרת, ובאנדרואיד אותו קמפיין לא מוצג שוב. אם 'dismissed' לא מגיע לעולם, ה-SDK משחרר את המשבצת אחרי 5 דקות.

טפסים בהודעות HTML​

כשאתם מציגים הודעת HTML בעצמכם, שלחו <form data-jry-form> שהיא מכילה דרך ה-SDK, כך שהערכים נשלחים עם הזהות הנוכחית ונבדקים מול הטופס שהקמפיין נושא:

const result = await Joryio.submitInAppForm(message.id, { email: '[email protected]' });
// { ok, message?, redirect?, errors? } - לעולם לא נדחה; כישלון הוא ok: false

סנכרון ובדיקות​

Joryio.syncInAppCampaigns(); // משיכת קמפיינים עכשיו והצגת אלה שמתאימים
Joryio.resetDisplayedCampaigns(); // לבדיקות בלבד: שכחת מה שהמכשיר הזה כבר הציג

ה-SDK כבר מסנכרן בתחילת סשן וכשהאפליקציה חוזרת לחזית; קראו ל-syncInAppCampaigns() לרגעים שביניהם. resetDisplayedCampaigns() פועל רק כש-enableDebug מופעל.

enableInAppMessages(capabilities?) ו-disableInAppMessages() הם הזוג ברמה הנמוכה ש-onInAppMessage קורא לו עבורכם; השתמשו בהם רק אם אתם מאזינים לאירוע JoryioInAppMessage בדרך אחרת. כש-JavaScript נטען מחדש, הגשר מחזיר את הרינדור ל-SDK הנייטיבי עד שהקוד שלכם נרשם שוב.

מתי הודעה נחשבת כמוצגת​

הודעה נחשבת כמוצגת - כלומר in_app.displayed מדווח ומגבלת התדירות של הקמפיין נצרכת - רק כשהאדם יכול היה לראות אותה. משך הזמן שעליה להיות על המסך הוא IN_APP_DISPLAY_DWELL_MS (1000 מילישניות), אותו קבוע בכל ה-SDK של Joryio.

  • הודעות שה-SDK הנייטיב מצייר פועלות לפי הכלל של iOS ו-Android: מוצגות רק במסך יציב שבחזית, ונספרות אחרי שנייה אחת על המסך כשהאפליקציה בחזית. הקשה או סגירה לפני כן נחשבות כצפייה; הודעה שיורדת מהמסך מוקדם יותר לא נספרת ונשארת זכאית.
  • אם אתם מציירים את ההודעות בעצמכם (onInAppMessage), דווחו 'displayed' לפי אותו כלל - IN_APP_DISPLAY_DWELL_MS מיוצא: אחרי שההודעה הייתה גלויה כל הזמן הזה, או ממש לפני לחיצה או סגירה שמגיעות מוקדם יותר. אל תדווחו אותו על הודעה שירדה מהמסך קודם.

אפשרויות הגדרה​

אפשרותסוגברירת מחדלתיאור
enableDebugbooleanfalseהפעלת יומני ניפוי שגיאות
logLevelstringנגזר מ-enableDebugרמת לוג: debug, info, warn, error. אם לא הוגדר, שני הגשרים הנייטיביים משתמשים ב-'debug' כאשר enableDebug הוא true וב-'error' אחרת - אין ברירת מחדל קבועה. הגדירו אותו רק כדי לדרוס זאת.
batchSizenumber50אירועים לאצווה לפני שליחה אוטומטית (ברירת מחדל נייטיב)
flushIntervalnumber5000מרווח שליחה אוטומטית במילישניות (ברירת מחדל נייטיב)
sendImmediatelybooleanfalseשליחת כל אירוע מיד עם המעקב, ללא אצווה (לא מומלץ לייצור)
maxQueueSizenumber1000מספר האירועים המרבי שהתור המקומי מחזיק בזמן שהשליחות נכשלות; מעבר לו הישנים ביותר נזרקים (ברירת מחדל נייטיב)
requestTimeoutnumber10000פסק זמן HTTP לכל בקשה במילישניות (ברירת מחדל נייטיב)
sessionTimeoutnumber1800000פסק זמן של סשן במילישניות (ברירת מחדל נייטיב: 30 דקות)
trackSessionStartbooleantrueמעקב אוטומטי אחר תחילת סשן
userIdstringnullהגדרת מזהה משתמש מראש
anonymousIdstringנוצר אוטומטיתמזהה אנונימי משלכם למכשיר זה. משמש להרצה זו ונשמר רק כאשר עדיין לא נשמר דבר, כך שמזהה שהוגדר פעם אחת שורד גם אם האפליקציה תפסיק להעביר את האפשרות
requestPushPermissionAtLaunchbooleanfalseבקשת הרשאת התראות במהלך initialize. עדיף לבקש ברגע שהמשתמש מבין למה
allowHtmlJsInAppMessagesbooleanfalseמאפשר ל-SDK הנייטיב לצייר הודעות In-App מסוג HTML (ראו הפעלת הודעות HTML)
holdInAppMessagesForHandlerbooleanfalseהחל מגרסה 1.3.2. האפליקציה שלכם מציירת הודעות In-App ונרשמת אחרי ש-initialize() הסתיים: הודעות מוחזקות עד ההרשמה (ראו מתי להירשם)
enableSdkAuthenticationbooleanfalseצירוף JWT שהונפק על ידיכם לכל בקשת שליחה
sdkAuthenticationTokenstring-ה-JWT ההתחלתי כאשר enableSdkAuthentication פעיל
trackingConsent'granted' | 'denied' | 'pending'לא מוגדרהסכמה למעקב בעת האתחול. השאירו לא מוגדר אלא אם יש לכם תשובה מחלון ההסכמה - ראו הסכמה בעת האתחול
optOutbooleanfalseהתחלה עם הסכמה מבוטלת למעקב
notificationChannelNamestring"Notifications"Android בלבד: שם ערוץ ההתראות שמשתמשים רואים בהגדרות המערכת (Android 8 ומעלה). העבירו אותו מתורגם. ב-iOS אין לו השפעה

כלי עזר​

// שליחת אירועים מיידית (לפני סגירת אפליקציה, התנתקות וכו')
Joryio.flush();

// קבלת מזהים
const anonymousId = await Joryio.getAnonymousId();
const userId = await Joryio.getUserId(); // null אם לא מזוהה
const sessionId = await Joryio.getSessionId();

תמיכת TypeScript​

ה-SDK כולל טיפוסים מלאים. ייבאו לפי הצורך:

import Joryio, {
type JoryioConfig,
type UserAttributes,
type InAppMessage,
} from '@joryio/react-native-sdk';

הסכמה למעקב​

שני מתגים נפרדים קובעים אם ה-SDK אוסף משהו, וכל "לא" גובר:

  • הסכמה - התשובה מחלון ההסכמה שלכם: 'granted', 'denied' או 'pending'. נקבעת עם trackingConsent ב-initialize() או עם updateConsent().
  • סירוב (opt-out) - הבחירה של האדם עצמו. נקבע עם optOut() ומוסר רק עם optIn().

ה-SDK אוסף רק כשההסכמה היא granted והאדם לא סירב. מתן הסכמה לעולם לא מבטל סירוב, וסירוב לעולם לא משנה את ההסכמה השמורה.

עצירה וחידוש של איסוף הנתונים. מדובר בהסכמה למעקב, לא במנוי שיווקי - optOut() אינו מסיר אף אחד מרשימת הדיוור. מצב המנוי נשמר על איש הקשר ומשתנה דרך ה-API או מרכז ההעדפות.

// המשתמש סירב למעקב
Joryio.optOut();

// ...ואחר כך הסכים
Joryio.optIn();

const optedOut = await Joryio.isUserOptedOut(); // מתג הסירוב בלבד
const consent = await Joryio.getTrackingConsent(); // 'granted' | 'denied' | 'pending'

wipeData() מוחק את כל מה שהמכשיר שומר על האדם הנוכחי - זהות, אירועים בתור, כתיבות ממתינות, מצב הודעות בתוך האפליקציה, סשן, מזהה מכשיר וטוקן פוש - ומבטל את רישום הטוקן בשרת. הסירוב נשמר, כך שמי שסירב וביקש מחיקה נשאר מסורב. הפוש נשאר כבוי עד registerPushToken() או מתן הרשאה.

optOut() רושם את ההחלטה בפרופיל כ-tracking_opted_out לפני שהוא מפסיק לשלוח, כך שאפשר להחריג בקהל את מי שסירב. optIn() מנקה את הסימון. זהו סימן לטירגוט ולא אכיפה - חסימה בצד השרת היא החלטה נפרדת.

אירועים שכבר בתור ולא נשלחו נמחקים על ידי optOut() ולא נשמרים למועד מאוחר יותר.

כדי להחליט לפני שמשהו נשלח, העבירו את ההסכמה ל-initialize() ועדכנו אותה מחלון ההסכמה שלכם:

await Joryio.initialize(sdkKey, apiHost, { trackingConsent: 'pending' });

// מאוחר יותר
Joryio.updateConsent('granted'); // 'granted' | 'denied' | 'pending'
  • השאירו את trackingConsent לא מוגדר, אלא אם לחלון ההסכמה שלכם יש תשובה להפעלה הזו. כשהוא לא מוגדר ודבר לא נשמר, ההתנהגות היא כמו granted; denied שנשמר בהפעלה קודמת נשאר בתוקף.
  • denied נשמר במכשיר ונשאר בתוקף גם אחרי הפעלה מחדש. רק updateConsent('granted') או trackingConsent: 'granted' מפורש מסירים אותו.
  • pending חל על ההפעלה הנוכחית בלבד ולעולם לא מחליף denied שמור. כל עוד ההסכמה במצב pending דבר לא נשלח: אירועים ושינויי מאפיינים ממתינים במכשיר (עד maxQueueSize), ואין התחלת סשן, סנכרון In-App או רישום לפוש. updateConsent('granted') שולח את מה שהמתין; updateConsent('denied') מוחק אותו.
  • העבירו optOut: true כדי להתחיל במצב סירוב. הסירוב נשמר במכשיר ונשאר בתוקף גם אחרי הפעלה מחדש; optOut: false אינו מבטל optOut() קודם - קראו ל-optIn().
  • אם האפליקציה שלכם כבר יודעת את התשובה כשהיא עולה - למשל בחירה מחלון ההסכמה שהיא שמרה בעצמה - העבירו אותה כ-trackingConsent ל-initialize() במקום לקרוא ל-updateConsent() אחריו, כך שה-SDK מתחיל כשהתשובה הזו כבר בתוקף. אין חשיבות לאותיות גדולות או קטנות ('Denied' נקרא כ-'denied'); ערך לא מוכר מתעלמים ממנו עם אזהרה, וההסכמה נשארת לא מוגדרת.

את optOut(), optIn(), updateConsent() ו-wipeData() אפשר לקרוא לפני initialize(), והם מוחלים ראשונים: שום התחלת סשן, סנכרון In-App או רישום פוש לא יוצאים לפניהם. קריאות מוקדמות יותר שאוספות נתונים - אירועים, מאפיינים, כינויים, שינויי מנוי - נזרקות על ידי optOut(), updateConsent('denied') או wipeData() שבא אחריהן, במקום להישלח כשה-SDK מתחיל. גם קריאות כאלה שבוצעו בזמן opt-out או סירוב - גם לפני initialize(), בין שההחלטה התקבלה בהפעלה הזו ובין שנשמרה בהפעלה קודמת - נזרקות, ו-optIn() או updateConsent('granted') מאוחר יותר לא שולח אותן. identify נזרק רק על ידי wipeData(). setSdkAuthenticationToken() מוקדם יותר אף פעם לא עובר למבקר הבא: ב-iOS וב-Android כאחד המחיקה שומרת אותו רק לביטולים של האדם שנמחק, או זורקת אותו. אחרי optOut() או סירוב, מזהה המשתמש נשמר במכשיר, בלי להישלח, רק כדי שביטול מנוי יגיע לאדם הנכון. ביטולים הם החריג: setSubscription(channel, 'unsubscribed'), removeFromSubscriptionGroup ו-unregisterPush() שבוצעו קודם נשמרים ונשלחים.

החלטה שמתקבלת בזמן ש-initialize() רץ ואז נכשל מוחלת ברגע הכישלון, לפני ה-initialize() הבא: הניסיון החוזר מתחיל כשהיא כבר בתוקף.

אפשר לקרוא גם ל-isUserOptedOut() ול-getTrackingConsent() לפני initialize(). בזמן ש-initialize() רץ הן ממתינות לו; בכל מצב אחר - לפני initialize() ואחרי אתחול שנכשל - הן עונות מיד לפי מה שנשמר במכשיר, כך שסירוב או denied מהפעלה קודמת מוחזרים כבר מההתחלה. כשלא התקבלה החלטה ושום דבר לא נשמר, getTrackingConsent() מחזירה 'granted' - המצב שבו ה-SDK פועל - בשתי הפלטפורמות.

מנוי שיווקי​

האם מותר לשלוח הודעות לאדם הזה בערוץ מסוים. זו הסכמה שונה מ-optOut(), שעניינה האם מותר לעקוב אחריו - אחת אינה גוררת את השנייה, ואפשר לסרב למעקב ועדיין לרצות את הניוזלטר.

זה מה שמרכז העדפות בתוך האפליקציה כותב.

Joryio.setSubscription('email', 'unsubscribed');

קבוצות מנוי הן רשימות שאדם יכול להצטרף אליהן או לעזוב אותן בכל ערוץ:

Joryio.addToSubscriptionGroup('list_abc123', 'email');
Joryio.removeFromSubscriptionGroup('list_abc123', 'email');

ערוצים: email, sms, whatsapp, push, viber. סטטוסים: optedIn, subscribed, unsubscribed.

נדרש מזהה - הקריאה מדולגת, עם שגיאה ביומן, אם אין מזהה משתמש ואין מזהה אנונימי.

אימות SDK​

אופציונלי. כשהוא מופעל, בקשה שנושאת מזהה משתמש חייבת לשאת גם JWT שהשרת שלכם הנפיק לאותו משתמש; קריאות של מבקרים אנונימיים מתקבלות בלעדיו.

await Joryio.initialize(sdkKey, apiHost, {
enableSdkAuthentication: true,
sdkAuthenticationToken: initialToken,
});

const stop = Joryio.onSdkAuthenticationError(async (error) => {
// error.reason: 'missing' | 'invalid' | 'expired' | 'sub_mismatch' | 'unknown'
// error.refreshExhausted: true ברגע שה-SDK הפסיק לנסות שוב את הבקשה הזו
Joryio.setSdkAuthenticationToken(await fetchFreshToken());
});

קראו ל-setSdkAuthenticationToken(token) אחרי התחברות ובכל פעם שבידי ה-handler שלכם יש אסימון חדש; הוא מצורף לכל בקשה שאחריו. onSdkAuthenticationError מחזיר פונקציה לביטול ההרשמה.

אבחון​

const { apiEndpoint, lastError } = await Joryio.getDiagnostics();
// lastError: { message, isUnauthorized, isRetryable, at } - חוזר ל-null כשקריאה מצליחה

השתמשו בו כשהשילוב מתנהג לא כצפוי: מפתח SDK שגוי נראה מאותחל בזמן שכל בקשה נכשלת עם isUnauthorized: true.

בטוח מול null וקריאות מוקדמות​

  • כל קריאה בטוחה לפני ש-initialize() מסתיים: עד 1,000 קריאות נשמרות ומבוצעות לפי הסדר, והפונקציות שמחזירות ערך ממתינות עד 5 שניות לפני שהן מחזירות ברירת מחדל. מעבר ל-1,000 הישנות ביותר נזרקות, קריאות נתונים קודם: ביטול מנוי, unregisterPush() או החלטת פרטיות נזרקים רק כשלא נשאר שום דבר אחר.
  • null או undefined לעולם לא מפילים את האפליקציה. ערך הגדרה null פירושו ברירת המחדל; קריאה שחסר בה ארגומנט חובה (שם אירוע, מזהה משתמש, אסימון) רושמת אזהרה ולא עושה דבר; ערך מאפיין null מסיר את המאפיין.
  • אם initialize() נכשל, הקריאות נשמרות באותו אופן ונשלחות כש-initialize() מאוחר יותר מצליח - אפשר לקרוא לו שוב עם ערכים מתוקנים. מאזינים שרשמתם (onInAppMessage, onSdkAuthenticationError) וביטולי מנוי שבוצעו בינתיים נשמרים ולא הולכים לאיבוד. החלטת פרטיות שהתקבלה בזמן ש-initialize() עוד רץ מוחלת ברגע הכישלון, כך שהניסיון החוזר מתחיל איתה.
  • שתי קריאות initialize() חופפות - React StrictMode מריץ effects פעמיים - חולקות הפעלה אחת. קריאה מאוחרת יותר נדחית בשקט וה-SDK ממשיך לרוץ; הגדרות ששונו דורשות הפעלה מחדש של האפליקציה.
  • NaN ו-Infinity לעולם לא נשלחים: ערך מאפיין או מאפיין אירוע שמכיל אחד מהם מושמט עם אזהרה, ושאר הקריאה מתבצעת. setAttribute(key, NaN) לא עושה דבר, במקום להסיר את המאפיין. גם ערכים בתוך מופעים של מחלקות שלכם - למשל שורת עגלה שנבנתה ממחלקה - נבדקים.
  • callback שאתם מעבירים ל-onInAppMessage או ל-onSdkAuthenticationError וזורק שגיאה (או מחזיר Promise שנדחה) נתפס ונרשם ביומן. הוא לעולם לא מפיל את האפליקציה ולא עוצר מאזינים אחרים.

מדריך API​

תחוםמתודות
הגדרהinitialize(sdkKey, apiHost, config?)
אירועיםtrack(eventName, properties?), trackScreen(screenName, properties?), flush()
זהותidentify(userId), addAlias(aliasLabel, aliasName), reset(), getUserId(), getAnonymousId(), getSessionId()
מאפייניםsetAttributes(attributes), setAttribute(key, value), incrementAttribute(key, by?), unsetAttribute(key)
פושregisterPushToken(token), unregisterPush(), reportPushDelivered(trackingId), trackPushClick(trackingId), getPushPermissionStatus(), requestPushPermission(), isPushEnabled()
In-ApponInAppMessage(callback, options?), trackInAppImpression(campaignId, action, elementId?), submitInAppForm(campaignId, values), syncInAppCampaigns(), resetDisplayedCampaigns(), enableInAppMessages(capabilities?), disableInAppMessages()
הסכמהoptOut(), optIn(), isUserOptedOut(), getTrackingConsent(), updateConsent(consent), wipeData()
מנוייםsetSubscription(channel, status), addToSubscriptionGroup(groupId, channel), removeFromSubscriptionGroup(groupId, channel)
אימות SDKsetSdkAuthenticationToken(token), onSdkAuthenticationError(callback)
מסחר אלקטרוניJoryio.ecommerce.* - ראו מסחר אלקטרוני
אבחוןgetDiagnostics()

בדיקות עם Jest​

ה-main של החבילה הוא JavaScript מהודר, ולכן הגדרות ה-Jest שלכם לא צריכות רשומת transformIgnorePatterns עבורה. בלי mock ל-NativeModules.JoryioModule כל קריאה היא no-op, ה-getters מחזירים את ערכי ברירת המחדל שלהם, ואף קריאה לא זורקת שגיאה.