.NET and Blazor quickstart
The WordsAreFlowing.Sdk package targets .NET 10 and ships a Blazor
component for the private ask. Its default storage is the browser's localStorage,
through Blazor WebAssembly.
1. Install
dotnet add package WordsAreFlowing.Sdk
The packages are on nuget.org. WordsAreFlowing.Sdk brings in
WordsAreFlowing.Sdk.Core, the engine every head shares.
2. Register
In Program.cs, call AddWordsAreFlowing
with your product's key. The options are a
WordsAreFlowingOptions;
ApiKey is the only required one, and registration throws an
ArgumentException without it.
using WordsAreFlowing.Sdk;
builder.Services.AddWordsAreFlowing(o =>
{
o.ApiKey = "waf_live_…";
o.AppVersion = "1.4.0";
});
This registers an IFeedbackLoop, the SDK's one object.
Every host-specific piece (storage, clock, prompter, transport) is registered with
TryAdd, so a registration you make before this call wins.
Not on Blazor WebAssembly? The default store needs WebAssembly's in-process
JavaScript runtime and throws elsewhere (Blazor Server, for instance). Register your own
IKeyValueStore before calling
AddWordsAreFlowing.
InMemoryKeyValueStore exists, but it keeps nothing
between runs, so occurrence counts and cooldowns start over each time.
3. Show the private ask
Put the FeedbackAsk component once in your layout and hand it the loop.
@using WordsAreFlowing.Sdk.Components
@using WordsAreFlowing.Sdk.Core.Engine
@inject IFeedbackLoop Loop
<FeedbackAsk Loop="@Loop" />
The component listens to DecisionMade and opens only when a
decision asks for it. It sends the note with
SubmitFeedbackAsync, and "Not now" calls
DismissAskAsync. It never calls
TrackAsync, so it can never cause a store prompt. Its copy is in
English or French, following the current UI culture; pass a
FeedbackAskTexts as Texts to change any
string.
On its first render the component also calls
InitializeAsync, which loads the cached rules and then
fetches fresh ones if the network allows. If you set InitializeOnFirstRender="false",
call it yourself; it is safe to call more than once. A rules change you make in the portal reaches
a device the next time InitializeAsync runs there.
4. Report triggers
Call TrackAsync where a behavioural moment happens in your
code — after the thing is done, not from a "rate us" button.
private async Task OnWorkoutSaved()
{
// ... after the save succeeded
var decision = await Loop.TrackAsync("workout_completed");
if (decision.Kind == DecisionKind.None)
Console.WriteLine($"Words Are Flowing: {decision.Reason}");
}
It counts one occurrence on the device, asks the engine, and returns a
Decision: its Kind is a
DecisionKind (None, AskRequested or
StorePromptFired), and for None its
Reason names the rule that blocked — the full list is in the
rules reference. It does not throw: a failure inside the loop
comes back as None with the reason error.
Your own ask instead of the component
Loop.DecisionMade += d =>
{
if (d.Kind == DecisionKind.AskRequested) ShowMyAsk(d.Trigger);
};
// when the user sends (rating is 1 to 5, or null)
var result = await Loop.SubmitFeedbackAsync(trigger, message, rating);
if (!result.Ok) ShowError(result.Code);
// when the user closes it without writing
await Loop.DismissAskAsync(trigger);
DecisionMade is raised after every
TrackAsync, including None decisions.
SubmitFeedbackAsync returns a
TransportResult; a failed note is not retried for you, so
keep what the user wrote and let them try again. When
Ok is false, Code carries the server's code
when it refused the note (for example feedback.empty for a blank message, or
feedback.too_long past 2,000 characters).
No store prompt on the web
A browser has no native review prompt. On the web a store-prompt rule whose conditions are met
ends in None with the reason prompter_unavailable.
If you want to point web users at your store listing, render a plain link with
StoreLinks — never a button that claims to open a review
prompt.
var ids = Loop.CurrentRules?.StoreIds;
var play = StoreLinks.GooglePlay(ids?.AndroidPackage); // null when not set
var appStore = StoreLinks.AppStore(ids?.IosAppId);
The store ids come from the product in the portal and travel inside the rules, available as
CurrentRules once
InitializeAsync has run.
Options
| Option | Meaning |
|---|---|
ApiKey | Required. The product's key. |
AppVersion | Sent with every note. |
UserId | Optional. Your own identifier for the user, sent with every note so you can find or delete their notes. Up to 128 characters from A–Z a–z 0–9 _ . : @ -. |
Attributes | Optional. Key/value pairs sent with every note and matched by rule conditions. Up to 20 keys; a value is a string (up to 200 characters), a boolean or a number. |
MaxStorePromptsPerYear | Lowers the store-prompt cap for this app. It can never raise it above 3. |
FlushThreshold | Counters are posted when this many distinct counters are queued. Default 10. FlushAsync posts them now, unless a flush failed in the last five minutes. |
BaseAddress | The ingest API. Defaults to the hosted service. |
Platform | Reported on notes. Defaults to web. |
Counters that cannot reach the server stay queued, and the SDK makes no new attempt for five minutes. A note that is sent successfully posts the queue with it, even inside those five minutes.