Recommendations
The ContactsManager SDK provides intelligent recommendation features to enhance your app’s social experience. These recommendations help users discover connections in different contexts, like finding users already using your app or suggesting people they might know.Types of Recommendations
The SDK offers several recommendation types:- Contacts to Invite: Identify contacts who would benefit from using your app
- App Users: Find contacts who are already using your app
- People You Might Know: Discover potential connections based on shared contacts
Contacts to Invite
Get a list of contacts that would be good to invite to your app:do {
let recommendations = try await ContactsService.shared
.getSharedContactsByUsersToInvite(limit: 10)
for recommendation in recommendations {
let contact = recommendation.contact
let score = recommendation.score
let reason = recommendation.reason
print("\(contact.displayName ?? "Unknown") - Score: \(score) - " +
"Reason: \(reason)")
}
} catch {
print("Error getting invitation recommendations: \(error.localizedDescription)")
}
import { getInviteRecommendations } from '@contactsmanager/rn';
try {
// Get invite recommendations with a limit of 10
const recommendations = await getInviteRecommendations(10);
// Display the recommendations
recommendations.forEach(recommendation => {
const { contact, score, reason } = recommendation;
console.log(`${contact.displayName || 'Unknown'} - Score: ${score} - Reason: ${reason}`);
});
} catch (error) {
console.error('Error getting invitation recommendations:', error);
}
try {
val recommendations = RecommendationService.getInstance(context)
.getSharedContactsByUsersToInvite(limit = 10)
.getOrThrow()
recommendations.forEach { recommendation ->
val contact = recommendation.contact
val score = recommendation.score
val reason = recommendation.reason
println("${contact.displayName ?: "Unknown"} - Score: $score - Reason: $reason")
}
} catch (e: Exception) {
println("Error getting invitation recommendations: ${e.localizedDescription}")
}
CMRecommendationService *recommendationService = [[CMContactService sharedInstance] recommendationService];
[recommendationService getSharedContactsByUsersToInviteWithLimit:10
completion:^(NSArray<CMContactRecommendation *> * _Nullable recommendations,
NSError * _Nullable error) {
if (error) {
NSLog(@"Error getting invitation recommendations: %@", error.localizedDescription);
return;
}
for (CMContactRecommendation *recommendation in recommendations) {
CMContact *contact = recommendation.contact;
double score = recommendation.score;
NSString *reason = recommendation.reason;
NSLog(@"%@ - Score: %.2f - Reason: %@",
contact.displayName ?: @"Unknown",
score,
reason);
}
}];
Recommendation Scores
Each recommendation includes a score between 0.0 and 1.0 that indicates how relevant the recommendation is. Higher scores indicate stronger recommendations.App Users
Find contacts who are already using your app:do {
let appUsers = try await ContactsService.shared.getContactsUsingApp(limit = 20)
print("Found \(appUsers.count) contacts using the app")
for appUser in appUsers {
let contact = appUser.contact
let organizationUserId = appUser.canonicalContact.organizationUserId
print("\(contact?.displayName ?? "Unknown") - " +
"User ID: \(organizationUserId)")
}
} catch {
print("Error getting app users: \(error.localizedDescription)")
}
import { getContactsUsingApp } from '@contactsmanager/rn';
try {
// Get contacts who are already using the app with a limit of 20
const appUsers = await getContactsUsingApp(20);
console.log(`Found ${appUsers.length} contacts using the app`);
// Display the app users
appUsers.forEach(appUser => {
const { contact, canonicalContact } = appUser;
const organizationUserId = canonicalContact.organizationUserId;
console.log(`${contact?.displayName || 'Unknown'} - User ID: ${organizationUserId}`);
});
} catch (error) {
console.error('Error getting app users:', error);
}
try {
val appUsers = RecommendationService.getInstance(context)
.getContactsUsingApp(limit = 20)
.getOrThrow()
println("Found ${appUsers.size} contacts using the app")
appUsers.forEach { appUser ->
val contact = appUser.contact
val organizationUserId = appUser.canonicalContact.organizationUserId
println("${contact?.displayName ?: "Unknown"} - User ID: $organizationUserId")
}
} catch (e: Exception) {
println("Error getting app users: ${e.localizedDescription}")
}
CMRecommendationService *recommendationService = [[CMContactService sharedInstance] recommendationService];
[recommendationService getContactsUsingAppWithLimit:20
completion:^(NSArray<CMLocalCanonicalContact *> * _Nullable contacts,
NSError * _Nullable error) {
if (error) {
NSLog(@"Error getting app users: %@", error.localizedDescription);
return;
}
NSLog(@"Found %lu contacts using the app", (unsigned long)contacts.count);
for (CMLocalCanonicalContact *appUser in contacts) {
CMContact *contact = appUser.contact;
NSString *organizationUserId = appUser.canonicalContact.organizationUserId;
NSLog(@"%@ - User ID: %@",
contact.displayName ?: @"Unknown",
organizationUserId);
}
}];
Using Organization User IDs
TheorganizationUserId property allows you to connect the local contact with their server-side identity, which is useful for social features.
People You Might Know
Discover potential connections based on mutual contact information:do {
let connections = try await ContactsService.shared.getUsersYouMightKnow(limit: 15)
print("Found \(connections.count) people you might know")
for user in connections {
print("\(user.fullName) - " +
"\(user.email ?? user.phone ?? "No contact info")")
}
} catch {
print("Error getting connection recommendations: \(error.localizedDescription)")
}
import { getUsersYouMightKnow } from '@contactsmanager/rn';
try {
// Get users you might know with a limit of 15
const connections = await getUsersYouMightKnow(15);
console.log(`Found ${connections.length} people you might know`);
// Display the people you might know
connections.forEach(user => {
console.log(`${user.fullName} - ${user.email || user.phone || 'No contact info'}`);
});
} catch (error) {
console.error('Error getting connection recommendations:', error);
}
try {
val connections = RecommendationService.getInstance(context)
.getUsersYouMightKnow(limit = 15)
.getOrThrow()
println("Found ${connections.size} people you might know")
connections.forEach { user ->
println("${user.fullName} - ${user.email ?: user.phone ?: "No contact info"}")
}
} catch (e: Exception) {
println("Error getting connection recommendations: ${e.localizedDescription}")
}
CMRecommendationService *recommendationService = [[CMContactService sharedInstance] recommendationService];
[recommendationService getUsersYouMightKnowWithLimit:15
completion:^(NSArray<CMCanonicalContact *> * _Nullable contacts,
NSError * _Nullable error) {
if (error) {
NSLog(@"Error getting connection recommendations: %@", error.localizedDescription);
return;
}
NSLog(@"Found %lu people you might know", (unsigned long)contacts.count);
for (CMCanonicalContact *user in contacts) {
NSString *contactInfo = user.email ?: user.phone ?: @"No contact info";
NSLog(@"%@ - %@", user.fullName, contactInfo);
}
}];
Building a Recommendation UI
Here’s an example of how to build a recommendations UI:struct RecommendationsView: View {
@State private var inviteRecommendations: [ContactRecommendation] = []
@State private var appUsers: [LocalCanonicalContact] = []
@State private var peopleYouMightKnow: [CanonicalContact] = []
@State private var isLoading = true
@State private var error: Error?
var body: some View {
ScrollView {
if isLoading {
ProgressView("Loading recommendations...")
.padding()
} else if let error = error {
VStack {
Text("Error loading recommendations")
.font(.headline)
Text(error.localizedDescription)
.foregroundColor(.red)
Button("Try Again") {
loadRecommendations()
}
.padding()
}
} else {
VStack(alignment: .leading, spacing: 20) {
// App Users Section
if !appUsers.isEmpty {
Text("People you know using the app")
.font(.headline)
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(appUsers, id: \.canonicalContact.id) { user in
AppUserCard(user: user)
}
}
.padding(.horizontal)
}
}
// People You Might Know Section
if !peopleYouMightKnow.isEmpty {
Text("People you might know")
.font(.headline)
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(peopleYouMightKnow, id: \.id) { user in
PeopleYouMightKnowCard(user: user)
}
}
.padding(.horizontal)
}
}
// Invite Recommendations Section
if !inviteRecommendations.isEmpty {
Text("Invite to the app")
.font(.headline)
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(inviteRecommendations, id: \.contact.id) {
recommendation in
InviteRecommendationCard(
recommendation: recommendation
)
}
}
.padding(.horizontal)
}
}
}
}
}
.navigationTitle("Recommendations")
.onAppear {
loadRecommendations()
}
}
}
class RecommendationsViewModel : ViewModel() {
private val recommendationService = RecommendationService.getInstance(context)
fun loadRecommendations(
onLoading: (Boolean) -> Unit,
onError: (String) -> Unit,
onSuccess: (List<ContactRecommendation>, List<LocalCanonicalContact>, List<CanonicalContact>) -> Unit
) {
viewModelScope.launch {
onLoading(true)
try {
// Load all recommendation types in parallel
val inviteDeferred = async { recommendationService.getSharedContactsByUsersToInvite(10).getOrThrow() }
val appUsersDeferred = async { recommendationService.getContactsUsingApp(20).getOrThrow() }
val peopleDeferred = async { recommendationService.getUsersYouMightKnow(15).getOrThrow() }
val (invite, users, people) = awaitAll(inviteDeferred, appUsersDeferred, peopleDeferred)
onSuccess(invite, users, people)
} catch (e: Exception) {
onError(e.localizedDescription)
} finally {
onLoading(false)
}
}
}
}
@Composable
fun RecommendationsScreen(
viewModel: RecommendationsViewModel = viewModel()
) {
var isLoading by remember { mutableStateOf(true) }
var error by remember { mutableStateOf<String?>(null) }
var inviteRecommendations by remember { mutableStateOf<List<ContactRecommendation>>(emptyList()) }
var appUsers by remember { mutableStateOf<List<LocalCanonicalContact>>(emptyList()) }
var peopleYouMightKnow by remember { mutableStateOf<List<CanonicalContact>>(emptyList()) }
LaunchedEffect(Unit) {
viewModel.loadRecommendations(
onLoading = { isLoading = it },
onError = { error = it },
onSuccess = { invite, users, people ->
inviteRecommendations = invite
appUsers = users
peopleYouMightKnow = people
}
)
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (error != null) {
ErrorView(
error = error!!,
onRetry = {
error = null
isLoading = true
viewModel.loadRecommendations(
onLoading = { isLoading = it },
onError = { error = it },
onSuccess = { invite, users, people ->
inviteRecommendations = invite
appUsers = users
peopleYouMightKnow = people
}
)
}
)
} else {
// App Users Section
if (appUsers.isNotEmpty()) {
RecommendationSection(
title = "People you know using the app",
items = appUsers
) { user ->
AppUserCard(user = user)
}
}
// People You Might Know Section
if (peopleYouMightKnow.isNotEmpty()) {
RecommendationSection(
title = "People you might know",
items = peopleYouMightKnow
) { user ->
PeopleYouMightKnowCard(user = user)
}
}
// Invite Recommendations Section
if (inviteRecommendations.isNotEmpty()) {
RecommendationSection(
title = "Invite to the app",
items = inviteRecommendations
) { recommendation ->
InviteRecommendationCard(recommendation = recommendation)
}
}
}
}
}
@interface CMRecommendationsViewController : UIViewController
@property (nonatomic, strong) CMRecommendationService *recommendationService;
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIStackView *contentStackView;
@property (nonatomic, strong) UIActivityIndicatorView *loadingIndicator;
@property (nonatomic, strong) UIView *errorView;
@property (nonatomic, strong) NSArray<CMContactRecommendation *> *inviteRecommendations;
@property (nonatomic, strong) NSArray<CMLocalCanonicalContact *> *appUsers;
@property (nonatomic, strong) NSArray<CMCanonicalContact *> *peopleYouMightKnow;
@end
@implementation CMRecommendationsViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Recommendations";
self.view.backgroundColor = UIColor.systemBackgroundColor;
// Initialize recommendation service
self.recommendationService = [[CMContactService sharedInstance] recommendationService];
// Setup UI
[self setupUI];
// Load recommendations
[self loadRecommendations];
}
- (void)setupUI {
// Setup scroll view
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.scrollView];
// Setup content stack view
self.contentStackView = [[UIStackView alloc] init];
self.contentStackView.axis = UILayoutConstraintAxisVertical;
self.contentStackView.spacing = 20;
self.contentStackView.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:self.contentStackView];
// Setup loading indicator
self.loadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleLarge];
self.loadingIndicator.translatesAutoresizingMaskIntoConstraints = NO;
self.loadingIndicator.hidesWhenStopped = YES;
[self.view addSubview:self.loadingIndicator];
// Setup constraints
[NSLayoutConstraint activateConstraints:@[
[self.scrollView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
[self.scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[self.contentStackView.topAnchor constraintEqualToAnchor:self.scrollView.topAnchor],
[self.contentStackView.leadingAnchor constraintEqualToAnchor:self.scrollView.leadingAnchor],
[self.contentStackView.trailingAnchor constraintEqualToAnchor:self.scrollView.trailingAnchor],
[self.contentStackView.bottomAnchor constraintEqualToAnchor:self.scrollView.bottomAnchor],
[self.contentStackView.widthAnchor constraintEqualToAnchor:self.scrollView.widthAnchor],
[self.loadingIndicator.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[self.loadingIndicator.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor]
]];
}
- (void)loadRecommendations {
[self.loadingIndicator startAnimating];
self.contentStackView.hidden = YES;
dispatch_group_t group = dispatch_group_create();
// Load invite recommendations
dispatch_group_enter(group);
[self.recommendationService getSharedContactsByUsersToInviteWithLimit:10 completion:^(NSArray<CMContactRecommendation *> * _Nullable recommendations, NSError * _Nullable error) {
if (!error) {
self.inviteRecommendations = recommendations;
}
dispatch_group_leave(group);
}];
// Load app users
dispatch_group_enter(group);
[self.recommendationService getContactsUsingAppWithLimit:20 completion:^(NSArray<CMLocalCanonicalContact *> * _Nullable contacts, NSError * _Nullable error) {
if (!error) {
self.appUsers = contacts;
}
dispatch_group_leave(group);
}];
// Load people you might know
dispatch_group_enter(group);
[self.recommendationService getUsersYouMightKnowWithLimit:15 completion:^(NSArray<CMCanonicalContact *> * _Nullable contacts, NSError * _Nullable error) {
if (!error) {
self.peopleYouMightKnow = contacts;
}
dispatch_group_leave(group);
}];
// Update UI when all requests complete
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[self.loadingIndicator stopAnimating];
[self updateUI];
});
}
- (void)updateUI {
// Clear existing content
for (UIView *view in self.contentStackView.arrangedSubviews) {
[view removeFromSuperview];
}
// Add sections
if (self.appUsers.count > 0) {
[self addSectionWithTitle:@"People you know using the app" items:self.appUsers type:CMRecommendationTypeAppUsers];
}
if (self.peopleYouMightKnow.count > 0) {
[self addSectionWithTitle:@"People you might know" items:self.peopleYouMightKnow type:CMRecommendationTypeUsersYouMightKnow];
}
if (self.inviteRecommendations.count > 0) {
[self addSectionWithTitle:@"Invite to the app" items:self.inviteRecommendations type:CMRecommendationTypeInviteRecommendations];
}
self.contentStackView.hidden = NO;
}
- (void)addSectionWithTitle:(NSString *)title items:(NSArray *)items type:(CMRecommendationType)type {
// Add section title
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.text = title;
titleLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightSemibold];
titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[titleLabel setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
UIView *titleContainer = [[UIView alloc] init];
[titleContainer addSubview:titleLabel];
[NSLayoutConstraint activateConstraints:@[
[titleLabel.topAnchor constraintEqualToAnchor:titleContainer.topAnchor constant:16],
[titleLabel.leadingAnchor constraintEqualToAnchor:titleContainer.leadingAnchor constant:16],
[titleLabel.trailingAnchor constraintEqualToAnchor:titleContainer.trailingAnchor constant:-16],
[titleLabel.bottomAnchor constraintEqualToAnchor:titleContainer.bottomAnchor]
]];
[self.contentStackView addArrangedSubview:titleContainer];
// Add horizontal scroll view for items
UIScrollView *horizontalScrollView = [[UIScrollView alloc] init];
horizontalScrollView.showsHorizontalScrollIndicator = NO;
horizontalScrollView.translatesAutoresizingMaskIntoConstraints = NO;
UIStackView *itemsStackView = [[UIStackView alloc] init];
itemsStackView.axis = UILayoutConstraintAxisHorizontal;
itemsStackView.spacing = 12;
itemsStackView.translatesAutoresizingMaskIntoConstraints = NO;
[horizontalScrollView addSubview:itemsStackView];
[NSLayoutConstraint activateConstraints:@[
[itemsStackView.topAnchor constraintEqualToAnchor:horizontalScrollView.topAnchor],
[itemsStackView.leadingAnchor constraintEqualToAnchor:horizontalScrollView.leadingAnchor constant:16],
[itemsStackView.trailingAnchor constraintEqualToAnchor:horizontalScrollView.trailingAnchor constant:-16],
[itemsStackView.bottomAnchor constraintEqualToAnchor:horizontalScrollView.bottomAnchor],
[itemsStackView.heightAnchor constraintEqualToAnchor:horizontalScrollView.heightAnchor]
]];
// Add items
for (id item in items) {
UIView *card = [self createCardViewForItem:item type:type];
[itemsStackView addArrangedSubview:card];
}
[self.contentStackView addArrangedSubview:horizontalScrollView];
[horizontalScrollView.heightAnchor constraintEqualToConstant:160].active = YES;
}
- (UIView *)createCardViewForItem:(id)item type:(CMRecommendationType)type {
UIView *card = [[UIView alloc] init];
card.backgroundColor = UIColor.systemBackgroundColor;
card.layer.cornerRadius = 8;
card.layer.shadowColor = UIColor.blackColor.CGColor;
card.layer.shadowOffset = CGSizeMake(0, 2);
card.layer.shadowRadius = 4;
card.layer.shadowOpacity = 0.1;
[card.widthAnchor constraintEqualToConstant:100].active = YES;
// Avatar container
UIView *avatarContainer = [[UIView alloc] init];
avatarContainer.translatesAutoresizingMaskIntoConstraints = NO;
avatarContainer.backgroundColor = UIColor.systemGray5Color;
avatarContainer.layer.cornerRadius = 40;
avatarContainer.clipsToBounds = YES;
[card addSubview:avatarContainer];
// Name label
UILabel *nameLabel = [[UILabel alloc] init];
nameLabel.translatesAutoresizingMaskIntoConstraints = NO;
nameLabel.textAlignment = NSTextAlignmentCenter;
nameLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
nameLabel.numberOfLines = 1;
[card addSubview:nameLabel];
// Action button
UIButton *actionButton = [UIButton buttonWithType:UIButtonTypeSystem];
actionButton.translatesAutoresizingMaskIntoConstraints = NO;
actionButton.layer.cornerRadius = 12;
actionButton.titleLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold];
[card addSubview:actionButton];
// Configure based on type
switch (type) {
case CMRecommendationTypeAppUsers: {
CMLocalCanonicalContact *user = (CMLocalCanonicalContact *)item;
nameLabel.text = user.contact.displayName ?: @"Unknown";
[actionButton setTitle:@"Follow" forState:UIControlStateNormal];
actionButton.backgroundColor = UIColor.systemBlueColor;
[actionButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
break;
}
case CMRecommendationTypeUsersYouMightKnow: {
CMCanonicalContact *user = (CMCanonicalContact *)item;
nameLabel.text = user.fullName;
[actionButton setTitle:@"Connect" forState:UIControlStateNormal];
actionButton.backgroundColor = UIColor.systemBlueColor;
[actionButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
break;
}
case CMRecommendationTypeInviteRecommendations: {
CMContactRecommendation *recommendation = (CMContactRecommendation *)item;
nameLabel.text = recommendation.contact.displayName ?: @"Unknown";
[actionButton setTitle:@"Invite" forState:UIControlStateNormal];
actionButton.backgroundColor = UIColor.systemGreenColor;
[actionButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
break;
}
}
// Layout constraints
[NSLayoutConstraint activateConstraints:@[
[avatarContainer.topAnchor constraintEqualToAnchor:card.topAnchor constant:8],
[avatarContainer.centerXAnchor constraintEqualToAnchor:card.centerXAnchor],
[avatarContainer.widthAnchor constraintEqualToConstant:80],
[avatarContainer.heightAnchor constraintEqualToConstant:80],
[nameLabel.topAnchor constraintEqualToAnchor:avatarContainer.bottomAnchor constant:8],
[nameLabel.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:4],
[nameLabel.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-4],
[actionButton.topAnchor constraintEqualToAnchor:nameLabel.bottomAnchor constant:8],
[actionButton.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:8],
[actionButton.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-8],
[actionButton.heightAnchor constraintEqualToConstant:32],
[actionButton.bottomAnchor constraintEqualToAnchor:card.bottomAnchor constant:-8]
]];
return card;
}
@end
Best Practices
- Load Recommendations Asynchronously: Use async/await to load recommendations without blocking the UI
- Implement Pagination: For large contact lists, use the
limitparameter to paginate results - Handle Permission Changes: Reload recommendations when contact permissions change
- Cache Results Temporarily: Avoid excessive API calls by caching results for a short period
- Show Loading States: Always show appropriate loading indicators during network operations
Troubleshooting
Common Issues
-
Empty Recommendations
- Ensure the user has granted contacts access
- Verify that contacts have been synced with
syncContacts() - Check if the user has enough contacts for meaningful recommendations
-
Missing User Information
- Make sure contacts have proper phone numbers or email addresses
- Ensure the server has up-to-date user information
-
Poor Recommendation Quality
- Increase the contacts sync frequency
- Encourage users to complete their profiles
- Consider implementing a feedback mechanism for recommendations