Accept and make connections
A connection opens in one of two directions. Either a peer sends you a request and you answer it,
or you call ConnectAsync on a peer. A peer is a nearby device that runs your app.
Answer an inbound request
Give the advertiser a decide callback. The plugin calls the callback one time for each request,
on a separate task, and applies the value that the callback returns:
trueaccepts the request.falsedeclines the request.nullholds the request for a person to answer later.
nearby.Advertiser.SetConsent(
NearbyConsentPolicy.Hold,
decide: async (device, cancellationToken) =>
{
// decide runs on a thread-pool thread, so dispatch to the UI thread first.
var prompt = _dispatcher.DispatchAsync(() => page.DisplayAlertAsync(
"Connect?", device.Presence.DisplayName, "Accept", "Decline"));
return await prompt.WaitAsync(cancellationToken);
});
await nearby.Advertiser.StartAsync(cancellationToken);
When the deadline of the request passes or advertising stops, the plugin cancels the token and
declines the request. MAUI alerts accept no token, so the example waits on the token with
WaitAsync. If the callback throws, the plugin logs the exception and holds the request.
A held request stays on its device. The device reports Status == RequestReceived, and
device.Request returns the request. A row can therefore call AcceptAsync or DeclineAsync from
its own buttons.
The callback can also answer without a prompt, for example from device.Presence.Info. Do not
treat that answer as authorization. device.Id lasts one session, and the peer chooses its own
presence, so neither is a credential. See
What the plugin does not give you and
Prove membership before connecting.
Each SetConsent call replaces both the policy and the decide callback. For the three policies,
see Consent policies.
Handle a request that expires
Every request has one deadline, which comes from the OfferWindow that the requesting device
declared. If nobody answers a held request before the deadline, the plugin declines the request and
the device returns to Visible.
request.Expired completes at the deadline. To prompt for a held request, race the prompt against
request.Expired:
var prompt = page.DisplayAlertAsync(
"Connect?", request.RemoteDevice.Presence.DisplayName, "Accept", "Decline");
if (await Task.WhenAny(prompt, request.Expired) != prompt)
{
// The request expired while the prompt was open. The plugin already declined it.
return;
}
await (await prompt ? request.AcceptAsync(ct) : request.DeclineAsync(ct));
An answer can still arrive a moment after the deadline. In that case, AcceptAsync throws
NearbyConnectException with TimedOut, so catch that exception. For a countdown, read
request.ExpiresIn from your own UI timer.
samples/NearbyChat/ViewModels/AdvertisedDeviceViewModel.cs
answers held requests from a row that counts down to request.ExpiresAt.
Connect to a device
Discovered devices appear in nearby.Devices with Status == Visible:
NearbyConnection connection = await device.ConnectAsync(cancellationToken);
You do not have to check Status first, and a second call is not an error:
- If the device is already connected,
ConnectAsyncreturns the existing connection. - If a handshake is already in progress,
ConnectAsyncjoins it. - If the device sent you a request,
ConnectAsyncaccepts that request.
A row that was bound earlier still works, because ConnectAsync finds the device in the session at
the moment you call it. To connect to an id that you stored, find the device in nearby.Devices
first.
Confirm the connection
The transport encrypts an open connection. The connection proves only that a device is on the other
end, not which person. connection.AuthenticationCode is six digits, identical on both ends. Show
the code on both screens, and have the two people read it to each other. Treat an empty code as a
mismatch.
The plugin does not enforce the comparison. If your app needs confirmation, block your own sends until a person confirms the code. See Compare the code.
Handle a connect that fails
If the handshake produces no connection, ConnectAsync throws NearbyConnectException and the
device returns to Visible. One exception type covers every connect failure, so branch on
Reason:
try
{
await device.ConnectAsync(cancellationToken);
}
catch (NearbyConnectException ex) when (ex.Reason is NearbyConnectFailure.TimedOut)
{
// Nobody answered within OfferWindow, or the device moved out of range. Retrying is reasonable.
}
catch (NearbyConnectException ex) when (ex.Reason is NearbyConnectFailure.Failed)
{
// The peer declined, or the handshake failed. The two are not distinguishable.
}
catch (NearbyConnectException ex) when (ex.Reason is NearbyConnectFailure.DeviceUnknown)
{
// The device left Devices before the handshake began.
}
NearbyConnectFailure has no Declined value. iOS does not tell the requesting device that a peer
declined, so the plugin reports Failed on both platforms. If you cancel your own token,
ConnectAsync throws OperationCanceledException instead.
Know when a connection closes
When a connection closes, its Closed task completes with the reason. The task never faults and is
never canceled:
NearbyEndReason reason = await connection.Closed;
For the four values, see Connection end reasons.
Every change to a device's Status also raises nearby.DeviceChanged. The plugin raises all
events from one loop, in order, and never on the UI thread. A slow handler delays every handler
after it, so keep handlers short and dispatch to the UI thread. To avoid the handler, bind to a
NearbyDeviceCollection. See Bind to a MAUI page.
Stop advertising or discovering
Advertising and discovery are independent, and a stop of one leaves the other active:
await nearby.Advertiser.StopAsync(); // Declines pending requests.
await nearby.Discoverer.StopAsync(); // Removes devices that were only Visible from Devices.
Neither stop closes an open connection. To also disconnect every peer, call StopAllAsync.
IsActive and the Changed event of each activity tell you whether it is active.
LastStop.Reason tells you how the activity last stopped: Requested, Faulted, or Suspended.
Faulted is the only reason that a user needs to see. To find what prevents a restart, call
CheckAvailabilityAsync.
Next
- Send and receive payloads after a connection opens.
- Troubleshooting if a connect never completes.