Nojoom 1.0.9
dotnet add package Nojoom --version 1.0.9
NuGet\Install-Package Nojoom -Version 1.0.9
<PackageReference Include="Nojoom" Version="1.0.9" />
<PackageVersion Include="Nojoom" Version="1.0.9" />
<PackageReference Include="Nojoom" />
paket add Nojoom --version 1.0.9
#r "nuget: Nojoom, 1.0.9"
#:package Nojoom@1.0.9
#addin nuget:?package=Nojoom&version=1.0.9
#tool nuget:?package=Nojoom&version=1.0.9
Nojoom Lifestyle SDK for .NET MAUI
Official .NET MAUI bindings for the Nojoom Lifestyle SDK. A single package embeds the Nojoom offers / coupons experience into a MAUI app on both Android and iOS.
You give the SDK a session token; it renders its entire UI itself — home, offer categories, merchant listings, offer details with maps, coupons, favourites, redemption (QR / code), and contacts. Your app builds none of those screens.
| Target | Included |
|---|---|
net10.0-android |
Managed binding + all runtime AARs (Jetpack Compose, Coil, Google Maps, Huawei Map Kit, Retrofit/OkHttp, Nimbus JWT) |
net9.0-ios |
Managed binding + native NojoomLifestyles.xcframework (device + simulator slices) |
One PackageReference covers both platforms.
Install
dotnet add package Nojoom --version 1.0.9
<PackageReference Include="Nojoom" Version="1.0.9" />
Requirements
- .NET 9 with the MAUI /
androidandiosworkloads. - Android minimum API level 24; iOS minimum 13.0.
What changed in 1.0.9
- Close (✕) button callback. Tapping the close button in the SDK header now notifies the
host —
INojoomSDKCallback.OnDismiss()on the Android direct-init path,NojoomActivity.SetOnDismiss(listener)viaNojoomActivity,NojoomSDK.SetOnDismiss(handler)on iOS. See Session callbacks. onRefreshTokenremoved (breaking). The SDK now manages the session token itself:verifyUserreturns a backend-issued session token, and on a 401/403 the SDK silently exchanges it for a fresh one against the backend's refresh endpoint. The host is no longer asked to refresh tokens. A 401/403 that cannot be refreshed now surfaces asonSessionExpired. Remove anyOnRefreshToken/INojoomRefreshTokenListener/INojoomTokenCallback/NojoomTokenCompletion/SetOnRefreshTokenusage — those types are gone from the binding.- iOS
NojoomSDKConfig— full config parity with Android'sNojoomConfig(timeout, diagnostics, screen-capture, integrity policy, App Attest). - Android
NojoomActivity.CreateIntentcarries the full config as flat arguments.
Host-app configuration
These cannot be carried by the package and must be supplied by the consuming app.
Android — required NuGet pins (build/runtime conflict fix)
com.google.gson and a few AndroidX versions conflict with what Microsoft.Maui.Core pins. Add
these to your app project or you'll hit NU1107 at build or a crash at runtime:
<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
<PackageReference Include="Xamarin.AndroidX.Lifecycle.LiveData" Version="2.11.0.1" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.LiveData.Core" Version="2.11.0.1" />
<PackageReference Include="Xamarin.AndroidX.SavedState.SavedState.Ktx" Version="1.5.0.1" />
</ItemGroup>
The NU1608 "version outside constraint" warnings these produce are expected and benign.
(Non-MAUI Xamarin.Android hosts must also add the GoogleGson NuGet ≥ 2.10.1 — a MAUI app
already provides it transitively.)
The SDK's networking stack also needs com.squareup.okio (Okio) present at runtime. Apps using
Firebase Analytics/Messaging already get it transitively via Xamarin.AndroidX.DataStore.*; if
yours doesn't, add Square.OkIO.JVM (≥ 3.17.0.1) yourself.
As of 1.0.7, Xamarin.AndroidX.NavigationEvent (supplies androidx.navigationevent.* for the
SDK's NavHost) ships as a direct package dependency — no host action needed. If you're on 1.0.5
or earlier and see java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/navigationevent/NavigationEventDispatcherOwner; at launch, upgrade, or add
Xamarin.AndroidX.NavigationEvent (1.0.2.1) yourself.
Crash on launch — IllegalStateException: Module with the Main dispatcher is missing: this
means your app resolved a different version of Xamarin.KotlinX.Coroutines.Android than
Xamarin.KotlinX.Coroutines.Core.Jvm (usually because another dependency — Firebase, Room,
WorkManager, anything that touches Kotlin coroutines — pulled one of the two to a different
version than the other). It's a host-side dependency conflict, not something inside this package.
Pin both explicitly to the same version in your app, and make sure the pins above are current —
stale/lower pin values can force the exact kind of conflict that leads here if worked around by
hand instead of raised.
Android — Google Maps API key (required)
The SDK shows a Google Map on offer-detail screens. Add your key to the host
AndroidManifest.xml (play-services-maps reads it from the host, never the SDK). Without it the
app hard-crashes with IllegalStateException: API key not found when a map opens.
<application ...>
<meta-data android:name="com.google.android.geo.API_KEY" android:value="YOUR_MAPS_KEY" />
</application>
Huawei devices additionally need an AppGallery Connect project (agconnect-services.json + Map
Kit key) in the host app.
iOS
No extra references or pins — the native NojoomLifestyles.xcframework ships inside the package
and links automatically.
Initialize & launch
The one runtime input is a session token from your own login flow. No API key or encryption
keys are needed — those are embedded in the SDK. Initialization is async — only show the SDK UI
after onSuccess.
Which backend does the SDK talk to?
If you do nothing, the SDK talks to the UAT environment
(https://uat.aideasolution.com/api/). For production, pass
https://prod.aideasolution.com/api/ as one extra argument exactly where shown in the two
code blocks below (CreateIntent on Android, Initialize on iOS). There's nothing else to
configure — no separate file or setting — it's that one line, right where you already call the
SDK.
Both uat.aideasolution.com and prod.aideasolution.com are certificate-pinned inside the
SDK, so no host-side network-security config is required for either.
⚠️ One rule: wherever you get the session
tokenfrom (your login API, or a test endpoint) must be the same backend you pass here. A token from one backend is always rejected on a different one — the SDK reports that asonSessionExpired/onError, which can look like a bug elsewhere but really just means the two don't match.
Android — launch NojoomActivity
using Com.Nojoom.Lifestyles.Public;
using Microsoft.Maui.ApplicationModel;
var activity = Platform.CurrentActivity;
// Callbacks can't ride inside a launch Intent, so register them via the SetOn* statics
// BEFORE StartActivity. onDismiss fires when the user taps the SDK's ✕ (Home tab only).
// Re-register before every launch — onDestroy() clears these registrations.
NojoomActivity.SetOnDismiss(new DismissListener(() => { /* e.g. refresh your own screen */ }));
// 2 arguments (token only) = talks to UAT automatically. To use a different backend,
// add the URL as a 3rd argument, e.g.:
// NojoomActivity.CreateIntent(activity, token, "https://prod.aideasolution.com/api/")
var intent = NojoomActivity.CreateIntent(activity, token);
activity.StartActivity(intent); // NojoomActivity self-initializes and renders its own UI
// SAM-interface adapter — INojoomDismissListener needs a real Java object to cross JNI.
class DismissListener : Java.Lang.Object, INojoomDismissListener
{
readonly Action _onDismiss;
public DismissListener(Action onDismiss) => _onDismiss = onDismiss;
public void OnDismiss() => _onDismiss();
}
iOS — initialize, present on success
using NojoomLifestyles;
using Microsoft.Maui.ApplicationModel;
using UIKit;
NojoomSDK.Initialize(
token,
baseUrl: null, // null = UAT automatically. Replace with e.g. "https://prod.aideasolution.com/api/"
// to use a different backend — that's the only change needed.
onSuccess: () => MainThread.BeginInvokeOnMainThread(() =>
{
// Grab the app's top-most view controller to present from.
var root = UIApplication.SharedApplication.KeyWindow?.RootViewController;
while (root?.PresentedViewController is not null)
root = root.PresentedViewController;
var vc = NojoomSDK.MakeContentViewController();
vc.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
root?.PresentViewController(vc, animated: true, completionHandler: null);
}),
onError: message => { /* show error, prompt re-login */ },
onSessionExpired: null,
onOfferRedeemed: null);
Session callbacks (optional, both platforms)
The SDK raises five host callbacks. In the native Kotlin/Swift API they are all just
parameters of initialize, side by side. The .NET binding keeps them together too — as the
five methods of INojoomSDKCallback (Android direct-init) — with one shape difference
forced by the binding layer, noted in the last column:
| Callback | When | Android via NojoomActivity |
Android via NojoomSDK.Initialize(context, config, callback) |
iOS |
|---|---|---|---|---|
onSuccess |
SDK ready | handled internally | INojoomSDKCallback.OnSuccess() |
Initialize arg |
onError |
init failed | Activity finishes itself | INojoomSDKCallback.OnError(msg) |
Initialize arg |
onSessionExpired |
session token dead & the SDK's own refresh failed, or idle timeout | SetOnSessionExpired(listener) |
INojoomSDKCallback.OnSessionExpired() |
Initialize arg |
onOfferRedeemed |
offer redeemed | SetOnOfferRedeemed(listener) |
INojoomSDKCallback.OnOfferRedeemed(token) |
Initialize arg |
onDismiss |
user tapped ✕ | NojoomActivity.SetOnDismiss(listener) — a static call, not an Initialize/CreateIntent arg |
INojoomSDKCallback.OnDismiss() — same interface, right beside the others |
NojoomSDK.SetOnDismiss(handler) — a separate static call, not an Initialize arg |
Token refresh is no longer a host callback. As of 1.0.9 the SDK owns the session-token lifecycle:
verifyUserhands it a backend session token, which it refreshes against the backend automatically on a 401/403. If a refresh is impossible the SDK firesonSessionExpired. Hosts only ever supply the initial Ooredoo token. The oldonRefreshToken/INojoomRefreshTokenListener/INojoomTokenCallback/NojoomTokenCompletion/SetOnRefreshTokensurface has been removed.
Why onDismiss looks different in .NET:
NojoomActivityregisters it the same way asonOfferRedeemed/onSessionExpired— a staticSetOnDismiss(listener)call, since a callback can't ride inside a launchIntent.NojoomActivityforwards it into the SDK's ownonDismissand fires it right before the Activity finishes itself. CallSetOnDismissagain before every subsequent launch —onDestroy()clears all threeSetOn*registrations, so a second launch without re-registering finishes the Activity with nothing delivered, which looks identical to a missing callback. Also note the ✕ only appears on the Home tab (detail screens show Back instead), and system back / swipe-back finishes the Activity directly without firingonDismiss— only the ✕ does. See the Android sample in this README andTestApp/NajoomSDKTestApp/Platforms/Android/ NajoomLauncher.csin the full deliverables for the working registration pattern.- iOS: the Objective-C bridge exposes
onDismissas a separatesetOnDismiss:selector rather than aninitializeargument. The Swift-nativeNojoomSDK.initializestill takes it as a parameter, like Kotlin — only the ObjC/.NET surface splits it out. - Android
INojoomSDKCallback: no difference —OnDismiss()is the fifth method on the same interface as the other four. Implement it exactly likeOnOfferRedeemed.
// iOS — the close button, hooked separately (before presenting MakeContentViewController)
NojoomSDK.SetOnDismiss(() => MainThread.BeginInvokeOnMainThread(() =>
UIApplication.SharedApplication.KeyWindow?.RootViewController?
.DismissViewController(true, null)));
Registering redeem / session-expired on the NojoomActivity path
Register these before launching NojoomActivity:
NojoomActivity.SetOnOfferRedeemed(new RedeemListener(token => { /* forward token to backend */ }));
NojoomActivity.SetOnSessionExpired(new SessionExpiredListener(() => { /* prompt re-login */ }));
RedeemListener and SessionExpiredListener are small adapter classes you add to your app —
each wraps a C# delegate in the SDK's Java interface and must extend Java.Lang.Object to cross
the JNI boundary. Paste these classes as-is:
using Com.Nojoom.Lifestyles.Public;
// Adapts an Action<string> to INojoomRedeemListener.
sealed class RedeemListener : Java.Lang.Object, INojoomRedeemListener
{
readonly Action<string> _onOfferRedeemed;
public RedeemListener(Action<string> onOfferRedeemed) => _onOfferRedeemed = onOfferRedeemed;
public void OnOfferRedeemed(string token) => _onOfferRedeemed(token);
}
// Adapts an Action to INojoomSessionExpiredListener.
sealed class SessionExpiredListener : Java.Lang.Object, INojoomSessionExpiredListener
{
readonly Action _onSessionExpired;
public SessionExpiredListener(Action onSessionExpired) => _onSessionExpired = onSessionExpired;
public void OnSessionExpired() => _onSessionExpired();
}
iOS — filling in the session callbacks
The iOS example above already uses the full Initialize overload (with baseUrl) but passed
null for the two optional callbacks. Fill them in like this:
NojoomSDK.Initialize(
token,
baseUrl: null, // or "https://prod.aideasolution.com/api/" — same as the example above
onSuccess: () => { /* present MakeContentViewController() */ },
onError: message => { /* show error */ },
onSessionExpired: () => { /* prompt re-login */ },
onOfferRedeemed: redeemToken => { /* forward opaque token to your backend */ });
// The close (✕) button is separate — it is NOT a parameter of Initialize:
NojoomSDK.SetOnDismiss(() => { /* dismiss/pop MakeContentViewController() */ });
Pass null for any callback you don't need. SetOnDismiss is optional too — without it the
SDK's view controller dismisses itself.
Behaviour notes
- The SDK owns the whole screen. Tapping its close (✕) button: via
NojoomActivitythe Activity finishes itself; viaNojoomSDK.Initialize(context, config, callback)yourINojoomSDKCallback.OnDismiss()fires; on iOS yourNojoomSDK.SetOnDismisshandler fires (or the view controller pops itself if you set none). The SDK stays initialised after a dismiss. - Host app-identity validation is enforced in release builds of
Nojoom 1.0.9(Androidom.ooredoo; iOSom.nawras.mynawras/com.cts.selfcare.ooredoo). It is skipped for debuggable Android hosts and the iOS Simulator, so day-to-day integration is unaffected. - Device-integrity checks run in log-only mode by default (never deny service); opt into
blocking with
NojoomConfig.integrityPolicy = NojoomIntegrityPolicy.Block(Android) /NojoomSDKConfig.IntegrityPolicyBlocks = true(iOS). - Screenshot/recording blocking is off by default — enable it per launch with
NojoomConfig.blockScreenCapture = true/NojoomActivity.CreateIntent's 4th argument /NojoomSDKConfig.BlockScreenCapture = true. - The session token carries a ~1-hour backend expiry plus a configurable idle timeout
(
sessionTimeoutMinutes, default 30). Both are handled by the SDK; expiry surfaces asonSessionExpired.
Configuration — both platforms
Android. NojoomActivity.CreateIntent carries the full config as of 1.0.9:
NojoomActivity.CreateIntent(activity, token,
baseUrl: "https://…/api/", blockScreenCapture: false,
sessionTimeoutMinutes: 30, diagnosticsLogging: false, integrityPolicyBlocks: false);
If you call NojoomSDK.Initialize(context, config, callback) directly, construct a NojoomConfig
— new NojoomConfig(token), new NojoomConfig(token, baseUrl), or the full named ctor
(token, blockScreenCapture, sessionTimeoutMinutes, diagnosticsLogging, integrityPolicy,
playIntegrityCloudProjectNumber, baseUrl). C# named arguments on these constructors work
as of 1.0.8 (≤ 1.0.7 emitted p0, p1, …, so named calls failed with CS1739).
iOS. As of 1.0.9 there is a NojoomSDKConfig object — parity with Android's NojoomConfig:
NojoomSDK.Initialize(
new NojoomSDKConfig { Token = token, BaseUrl = "https://…/api/",
SessionTimeoutMinutes = 30, DiagnosticsLogging = false,
BlockScreenCapture = false, IntegrityPolicyBlocks = false, AppAttestEnabled = false },
onSuccess, onError, onSessionExpired, onOfferRedeemed);
On ≤ 1.0.8 the iOS binding only accepted token + baseUrl; the Initialize(token, …) overloads
still work.
API surface
Android (Com.Nojoom.Lifestyles.Public) — NojoomActivity.CreateIntent(context, token[, baseUrl[, blockScreenCapture[, sessionTimeoutMinutes, diagnosticsLogging, integrityPolicyBlocks]]]),
SetOnOfferRedeemed, SetOnSessionExpired, SetOnDismiss; NojoomSDK.Initialize(context, config, callback) / IsInitialized / Reset; NojoomConfig, NojoomIntegrityPolicy;
INojoomSDKCallback (OnSuccess/OnError/OnSessionExpired/OnOfferRedeemed/OnDismiss),
INojoomRedeemListener, INojoomSessionExpiredListener, INojoomDismissListener. (NojoomSDKView
is intentionally not bound — use NojoomActivity.)
iOS (NojoomLifestyles) — NojoomSDK.Initialize(token, onSuccess, onError),
NojoomSDK.Initialize(token, baseUrl, onSuccess, onError, onSessionExpired, onOfferRedeemed),
NojoomSDK.Initialize(config, onSuccess, onError, onSessionExpired, onOfferRedeemed) +
NojoomSDKConfig, MakeContentViewController(), SetOnDismiss(handler), IsInitialized,
Reset().
License
Proprietary. All rights reserved. See LICENSE.txt.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0-ios18.0 is compatible. net10.0-android36.0 is compatible. net10.0-ios was computed. |
-
net10.0-android36.0
- Xamarin.AndroidX.Activity (>= 1.10.1.3)
- Xamarin.AndroidX.Compose.Runtime.Annotation.Android (>= 1.10.4.1)
- Xamarin.AndroidX.NavigationEvent (>= 1.0.2.1)
- Xamarin.Google.Android.Play.Core.Common (>= 2.0.4.7)
- Xamarin.Google.Android.Play.Integrity (>= 1.4.0.6)
- Xamarin.GooglePlayServices.Base (>= 118.2.0.6)
- Xamarin.GooglePlayServices.Basement (>= 118.7.1.1)
- Xamarin.GooglePlayServices.Maps (>= 118.2.0.2)
- Xamarin.GooglePlayServices.Tasks (>= 118.3.2.1)
- Xamarin.Kotlin.StdLib (>= 2.3.10.1)
- Xamarin.KotlinX.Coroutines.Android (>= 1.10.2.3)
- Xamarin.KotlinX.Serialization.Core.Jvm (>= 1.10.0.1)
-
net9.0-ios18.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.