Skip to content

Commit 4c845ca

Browse files
Added rating prompt
1 parent b56976b commit 4c845ca

File tree

4 files changed

+209
-0
lines changed

4 files changed

+209
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Threading.Tasks;
2+
3+
namespace Community.VisualStudio.Toolkit
4+
{
5+
/// <summary>
6+
/// An interface used to storing information related to <see cref="RatingPrompt"/>
7+
/// </summary>
8+
public interface IRatingConfig
9+
{
10+
/// <summary>
11+
/// The number of valid requests made to show the rating prompt.
12+
/// </summary>
13+
int RatingRequests { get; set; }
14+
15+
/// <summary>
16+
/// A method to asynchronously persist the <see cref="RatingRequests"/>.
17+
/// </summary>
18+
/// <returns></returns>
19+
Task SaveAsync();
20+
}
21+
}

src/toolkit/Community.VisualStudio.Toolkit.Shared/Notifications/InfoBar.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ public async Task<bool> TryShowInfoBarUIAsync()
135135
/// </summary>
136136
public bool TryGetWpfElement(out Control? control)
137137
{
138+
ThreadHelper.ThrowIfNotOnUIThread();
139+
138140
object? uiObject = null;
139141
control = null;
140142
_uiElement?.GetUIObject(out uiObject);
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Globalization;
4+
using System.Threading.Tasks;
5+
using System.Windows.Controls;
6+
using Microsoft.VisualStudio.Imaging;
7+
using Microsoft.VisualStudio.PlatformUI;
8+
using Microsoft.VisualStudio.Shell;
9+
using Task = System.Threading.Tasks.Task;
10+
11+
namespace Community.VisualStudio.Toolkit
12+
{
13+
/// <summary>
14+
/// A standardized way to prompt the user to rate and review the extension
15+
/// </summary>
16+
public class RatingPrompt
17+
{
18+
private const string _urlFormat = "https://marketplace.visualstudio.com/items?itemName={0}&ssr=false#review-details";
19+
private const int _minutesVisible = 2;
20+
private static bool _hasChecked;
21+
22+
/// <summary>
23+
/// Creates a new instance of the rating prompt.
24+
/// </summary>
25+
/// <param name="marketplaceId">The unique Marketplace id found at the end of the Marketplace URL. For instance: "MyName.MyExtensions".</param>
26+
/// <param name="extensionName">The name of the extension to show in the prompt.</param>
27+
/// <param name="config">Likely a options page that implements the <see cref="IRatingConfig"/> interface. This is used to keep track of how many times the prompt was requested and if the user has already rated.</param>
28+
/// <param name="requestsBeforePrompt">Indicates how many successful requests it takes before the user is prompted to rate.</param>
29+
/// <exception cref="ArgumentNullException">None of the parameters passed in can be null.</exception>
30+
/// <exception cref="ArgumentException">The Marketplace ID has to be valid so an absolute URI can be constructed.</exception>
31+
public RatingPrompt(string marketplaceId, string extensionName, IRatingConfig config, int requestsBeforePrompt = 5)
32+
{
33+
MarketplaceId = marketplaceId ?? throw new ArgumentNullException(nameof(marketplaceId));
34+
ExtensionName = extensionName ?? throw new ArgumentNullException(nameof(extensionName));
35+
Config = config ?? throw new ArgumentNullException(nameof(config));
36+
RequestsBeforePrompt = requestsBeforePrompt;
37+
38+
string ratingUrl = string.Format(CultureInfo.InvariantCulture, _urlFormat, MarketplaceId);
39+
40+
if (!Uri.TryCreate(ratingUrl, UriKind.Absolute, out Uri parsedUrl))
41+
{
42+
throw new ArgumentException($"{RatingUrl} is not a valid URL", nameof(marketplaceId));
43+
}
44+
45+
RatingUrl = parsedUrl;
46+
}
47+
48+
/// <summary>
49+
/// The Marketplace ID is the unique last part of the URL. For instance: "MyName.MyExtension".
50+
/// </summary>
51+
public virtual string MarketplaceId { get; }
52+
53+
/// <summary>
54+
/// The name of the extension. It's shown in the prompt so the user knows which extension to rate.
55+
/// </summary>
56+
public virtual string ExtensionName { get; }
57+
58+
/// <summary>
59+
/// The configuration/options object used to store the information related to the rating prompt.
60+
/// </summary>
61+
public virtual IRatingConfig Config { get; }
62+
63+
/// <summary>
64+
/// The Marketplace URL the users are taken to when prompted.
65+
/// </summary>
66+
public virtual Uri RatingUrl { get; }
67+
68+
/// <summary>
69+
/// Indicates how many successful requests it takes before the user is prompted to rate.
70+
/// </summary>
71+
public virtual int RequestsBeforePrompt { get; set; }
72+
73+
/// <summary>
74+
/// Registers successful usage of the extension. Only one is registered per Visual Studio session.
75+
/// When the number of usages matches <see cref="RequestsBeforePrompt"/>, the prompt will be shown.
76+
/// </summary>
77+
public virtual void RegisterSuccessfulUsage()
78+
{
79+
if (!_hasChecked && Config.RatingRequests < RequestsBeforePrompt)
80+
{
81+
_hasChecked = true;
82+
IncrementAsync().FireAndForget();
83+
}
84+
}
85+
86+
/// <summary>
87+
/// Resets the count of successful usages and starts over.
88+
/// </summary>
89+
public virtual async Task ResetAsync()
90+
{
91+
Config.RatingRequests = 0;
92+
await Config.SaveAsync();
93+
}
94+
95+
private async Task IncrementAsync()
96+
{
97+
await System.Threading.Tasks.Task.Yield(); // Yield to allow any shutdown procedure to continue
98+
99+
if (VsShellUtilities.ShellIsShuttingDown)
100+
{
101+
return;
102+
}
103+
104+
Config.RatingRequests += 1;
105+
await Config.SaveAsync();
106+
107+
if (Config.RatingRequests == RequestsBeforePrompt)
108+
{
109+
PromptAsync().FireAndForget();
110+
}
111+
}
112+
113+
private async Task PromptAsync()
114+
{
115+
InfoBar? infoBar = await CreateInfoBarAsync();
116+
117+
if (infoBar == null)
118+
{
119+
return;
120+
}
121+
122+
if (await infoBar.TryShowInfoBarUIAsync())
123+
{
124+
if (infoBar.TryGetWpfElement(out Control? control))
125+
{
126+
control?.SetResourceReference(Control.BackgroundProperty, EnvironmentColors.SearchBoxBackgroundBrushKey);
127+
}
128+
129+
// Automatically close the InfoBar after a period of time
130+
await Task.Delay(_minutesVisible * 60 * 1000);
131+
132+
if (infoBar.IsVisible)
133+
{
134+
await ResetAsync();
135+
infoBar.Close();
136+
}
137+
}
138+
}
139+
140+
private async Task<InfoBar?> CreateInfoBarAsync()
141+
{
142+
InfoBarModel model = new(
143+
new[] {
144+
new InfoBarTextSpan("Are you enjoying the "),
145+
new InfoBarTextSpan(ExtensionName, true),
146+
new InfoBarTextSpan(" extension? Help spread the word by leaving a review.")
147+
},
148+
new[] {
149+
new InfoBarHyperlink("Rate it now"),
150+
new InfoBarHyperlink("Remind me later"),
151+
new InfoBarHyperlink("Don't show again"),
152+
},
153+
KnownMonikers.Extension,
154+
true);
155+
156+
InfoBar? infoBar = await VS.InfoBar.CreateAsync(model);
157+
158+
if (infoBar != null)
159+
{
160+
infoBar.ActionItemClicked += ActionItemClicked;
161+
}
162+
163+
return infoBar;
164+
}
165+
166+
private void ActionItemClicked(object sender, InfoBarActionItemEventArgs e)
167+
{
168+
ThreadHelper.ThrowIfNotOnUIThread();
169+
170+
if (e.ActionItem.Text == "Rate it now")
171+
{
172+
Process.Start(RatingUrl.OriginalString);
173+
}
174+
else if (e.ActionItem.Text == "Remind me later")
175+
{
176+
ResetAsync().FireAndForget();
177+
}
178+
179+
e.InfoBarUIElement.Close();
180+
181+
((InfoBar)sender).ActionItemClicked -= ActionItemClicked;
182+
}
183+
}
184+
}

src/toolkit/Community.VisualStudio.Toolkit.Shared/VSSDK.Helpers.Shared.projitems

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@
5959
<Compile Include="$(MSBuildThisFileDirectory)MEF\TokenTaggerBase.cs" />
6060
<Compile Include="$(MSBuildThisFileDirectory)MEF\InternalTaggerBase.cs" />
6161
<Compile Include="$(MSBuildThisFileDirectory)MEF\WpfTextViewCreationListener.cs" />
62+
<Compile Include="$(MSBuildThisFileDirectory)Notifications\IRatingConfig.cs" />
6263
<Compile Include="$(MSBuildThisFileDirectory)Notifications\InfoBar.cs" />
6364
<Compile Include="$(MSBuildThisFileDirectory)Notifications\MessageBox.cs" />
65+
<Compile Include="$(MSBuildThisFileDirectory)Notifications\RatingPrompt.cs" />
6466
<Compile Include="$(MSBuildThisFileDirectory)Notifications\StatusBar.cs" />
6567
<Compile Include="$(MSBuildThisFileDirectory)Options\BaseOptionModel.cs" />
6668
<Compile Include="$(MSBuildThisFileDirectory)Options\BaseOptionPage.cs" />

0 commit comments

Comments
 (0)