Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.github/**
.vscode/**
.vscode-test/**
images/**
out/test/**
src/**
.gitignore
Expand Down
614 changes: 614 additions & 0 deletions assets/sponsorware.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
"vscode-test": "^1.4.0"
},
"dependencies": {
"axios": "^0.20.0",
"iconv-lite": "^0.6.2",
"jsonfile": "^6.0.1",
"uuid": "^8.3.0",
Expand Down
1 change: 1 addition & 0 deletions src/IISExpress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export class IISExpress {

// Log telemtry
telemtry.updateCountAndReport(this._context, this._reporter, telemtry.keys.start);
telemtry.updateCountAndReport(this._context, this._reporter, telemtry.keys.sponsorware);

// This is the magic that runs the IISExpress cmd from the appcmd config list
this._iisProcess = process.spawn(this._iisPath, [`-site:${siteName}`]);
Expand Down
115 changes: 115 additions & 0 deletions src/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as vscode from 'vscode';
import axios from 'axios';
import TelemetryReporter from 'vscode-extension-telemetry';

// The GitHub Authentication Provider accepts the scopes described here:
// https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
const SCOPES = ['read:user', 'read:org'];
const GITHUB_AUTH_PROVIDER_ID = 'github';
const AZURE_FUNCTION_URL = 'https://warren-buckley.co.uk/IsActiveSponsor';

export class Credentials {

private authSession: vscode.AuthenticationSession | undefined;
private reporter: TelemetryReporter | undefined;
private context: vscode.ExtensionContext;

async initialize(context: vscode.ExtensionContext, reporter:TelemetryReporter): Promise<void> {
this.registerListeners(context);
this.setAuthSession();
this.reporter = reporter;
this.context = context;
}

private async setAuthSession() {
/**
* By passing the `createIfNone` flag, a numbered badge will show up on the accounts activity bar icon.
* An entry for the sample extension will be added under the menu to sign in. This allows quietly
* prompting the user to sign in.
* */
const session = await vscode.authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, { createIfNone: false });

if (session) {
this.authSession = session;
return;
}

this.authSession = undefined;
}

private registerListeners(context: vscode.ExtensionContext): void {
/**
* Sessions are changed when a user logs in or logs out.
*/
context.subscriptions.push(vscode.authentication.onDidChangeSessions(async e => {
if (e.provider.id === GITHUB_AUTH_PROVIDER_ID) {
await this.setAuthSession();

// If logging in or out - reset the GlobalState config (no way in event to be notified if login or logout)
this.context.globalState.update('iisexpress.sponsorware.login.cancelled', false);
}
}));
}

// Acessed from sponsorware class
async isUserSponsor():Promise<boolean> {
// Ensure we have an auth session
if(this.authSession === undefined){

const userHasCancelledLogin = this.context.globalState.get<Boolean>('iisexpress.sponsorware.login.cancelled', false);
const promptWithLoginDialog = !userHasCancelledLogin;

/**
* When the `createIfNone` flag is passed, a modal dialog will be shown asking the user to sign in.
* Note that this can throw if the user clicks cancel.
*/
try {
const session = await vscode.authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, { createIfNone: promptWithLoginDialog });
this.authSession = session;
} catch (error) {
// Store something in storage to say user explicitly cancelled/said NO
// Then above we can toggle createIfNone based on this value - so we don't annolying re-prompt to login in all the time
this.context.globalState.update('iisexpress.sponsorware.login.cancelled', true);

// User has explicitly not consented to allow us to login
// We will need to show the sponsorware message
return false;
}
}

// Use the token in authSession
const accessToken = this.authSession?.accessToken;
let isValidSponsor = false;

if(accessToken){
// Make a request to Azure Function to check if they are a sponsor
// Do request - pass back JSON response bool from this func
await axios.post(AZURE_FUNCTION_URL, {token: accessToken})
.then(response => {
isValidSponsor = response.data.validSponsor ? response.data.validSponsor : false;
})
.catch(error => {

// if (error.response) {
// // The request was made and the server responded with a status code
// // that falls out of the range of 2xx
// console.log(error.response.data);

// } else if (error.request) {
// // The request was made but no response was received
// // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// // http.ClientRequest in node.js
// console.log(error.request);
// } else {
// // Something happened in setting up the request that triggered an Error
// console.log("Error", error.message);
// }

this.reporter?.sendTelemetryException(error);
isValidSponsor = false;
});
}

return isValidSponsor;
}
}
19 changes: 18 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import TelemetryReporter from 'vscode-extension-telemetry';
import * as iis from './IISExpress';
import * as verify from './verification';
import * as settings from './settings';
import * as util from './util';
import { ControlsTreeProvider } from './ControlsTreeProvider';
import { Credentials } from './credentials';
import { Sponsorware } from './sponsorware';


let iisExpressServer:iis.IISExpress;
Expand All @@ -22,7 +25,6 @@ const extensionVersion = pkgJson.version;
// the application insights key (also known as instrumentation key)
const key = 'e0cc903f-73ec-4216-92cd-3479696785b2';


// telemetry reporter
// create telemetry reporter on extension activation
const reporter:TelemetryReporter = new TelemetryReporter(extensionId, extensionVersion, key);
Expand All @@ -31,11 +33,20 @@ const reporter:TelemetryReporter = new TelemetryReporter(extensionId, extensionV
// your extension is activated the very first time the command is executed
export async function activate(context: vscode.ExtensionContext) {

// Get a random number to use/compare if we have run IIS Express
const randomNumberOfLaunchesToShowSponsor = util.getRandomIntInclusive(5, 20);
context.globalState.update('iisexpress.sponsorware.display.count', randomNumberOfLaunchesToShowSponsor);

// This will check if the user has VS LiveShare installed & return its API to us
// If not then this will be null
const liveshare = await vsls.getApi();
let liveShareServer:vscode.Disposable;

// Init credentials class with event listener & prompt/get token from GitHub auth
const credentials = new Credentials();
await credentials.initialize(context, reporter);
const sponsorware = new Sponsorware(context, credentials);

// Register tree provider to put our custom commands into the tree
// Start, Stop, Restart, Support etc...
const controlsTreeProvider = new ControlsTreeProvider();
Expand All @@ -58,6 +69,9 @@ export async function activate(context: vscode.ExtensionContext) {
// Pass settings - just in case its changed between session
iisExpressServer.startWebsite(settings.getSettings());

// Checks if we need to display sponsoware webview message
await sponsorware.showSponsorMessagePanel();

// Ensure user has liveshare extension
if(liveshare !== null){
if((liveshare.session.id !== null) && (liveshare.session.role === vsls.Role.Host)){
Expand Down Expand Up @@ -120,6 +134,9 @@ export async function activate(context: vscode.ExtensionContext) {
// Open site in browser - this will need to check if site is running first...
// Pass settings - just in case its changed between session (Hence not set globally in this file)
iisExpressServer.restartSite(settings.getSettings());

// Checks if we need to display sponsoware webview message
await sponsorware.showSponsorMessagePanel();
});

const supporter = vscode.commands.registerCommand('extension.iis-express.supporter',async () => {
Expand Down
10 changes: 2 additions & 8 deletions src/settings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as vscode from 'vscode';
import * as util from './util';

export interface Isettings {
port: number;
Expand Down Expand Up @@ -102,12 +103,5 @@ export function getSettings():Isettings{
// IIS Express docs recommend ports greater than 1024
// http://www.iis.net/learn/extensions/using-iis-express/running-iis-express-without-administrative-privileges
export function getRandomPort():number{
return getRandomIntInclusive(1024,44399);
}


// Returns a random integer between min (included) and max (included)
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
function getRandomIntInclusive(min:number, max:number):number {
return Math.floor(Math.random() * (max - min + 1)) + min;
return util.getRandomIntInclusive(1024,44399);
}
121 changes: 121 additions & 0 deletions src/sponsorware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import * as vscode from 'vscode';
import path = require('path');
import { Credentials } from './credentials';

export class Sponsorware {

private context: vscode.ExtensionContext;
private totalCount:number = 0;
private credentials: Credentials;

constructor(context: vscode.ExtensionContext, credentials:Credentials) {
this.context = context;
this.credentials = credentials;
}

private async doWeShowSponsorMessagePanel():Promise<boolean> {
// Exit out early if they are a sponsor
// Don't show them the sponsorware message

const isSponsor = await this.credentials.isUserSponsor();
if(isSponsor){
return false;
}

// Get the counter values
this.totalCount = this.context.globalState.get<number>('iisexpress.start.count', 0);
const sponsorwareCount = this.context.globalState.get<number>('iisexpress.sponsorware.count', 0);
const sponsorwareDisplayCount = this.context.globalState.get<number>('iisexpress.sponsorware.display.count', 10); // Default to 10 if the random number not been set

// Decide if we met the threshold yet
if(sponsorwareCount >= sponsorwareDisplayCount){
// Each activation of extension (ie when VSCode boots)
// Will decide a random number in a range as the threshold counter
// So its not always the same number of launches of a site

// If we have met the threshold - reset the sponsor counter back to 0
this.context.globalState.update('iisexpress.sponsorware.count', 0);
return true;
}
else {
return false;
}
}

private getWebviewContent(numberOfLaunches:number, svgSrc:vscode.Uri) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IIS Express - Sponsorware</title>
<style>
h1 span {
color: var(--vscode-terminal-ansiBrightYellow);
}

a.button {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
padding: 10px;
border-radius: 7px;
font-weight: 700;
text-decoration: none;
display:inline-block;
margin-top: 20px;
}

a.button:hover {
background-color: var(--vscode-button-hoverBackground);
}

img {
width:60%;
float:right;
}
</style>
</head>
<body>
<img src="${svgSrc}" />
<h1>You have used IIS Express <span>${numberOfLaunches}</span> times.</h1>
<p>I'm happy to see you're using IIS Express extension alot, have you considered becoming a GitHub sponsor for this project?</p>
<p>Becoming a sponsor removes this sponsorware message and you get a warm fuzzy feeling for supporting an individual.</p>

<p>
<a href="https://github.com/sponsors/warrenbuckley"
class="button"
role="button"
title="Sponsor Warren Buckley">Sponsor Warren Buckley on GitHub</a>
</p>
</body>
</html>`;
}

async showSponsorMessagePanel():Promise<void> {

// Exit out early (if user is a sponsor or the counter threshold not met yet)
if(await this.doWeShowSponsorMessagePanel() === false){
return;
}

// Create and show a new webview
const panel = vscode.window.createWebviewPanel(
'iisExpress.sponsorware',
'IIS Express - Sponsorware',
vscode.ViewColumn.Beside,
{
// Only allow the webview to access resources in our extension's media directory
localResourceRoots: [vscode.Uri.file(path.join(this.context.extensionPath, 'assets'))]
}
);

const onDiskPath = vscode.Uri.file(path.join(this.context.extensionPath, 'assets', 'sponsorware.svg'));
const sponsorSvgSrc = panel.webview.asWebviewUri(onDiskPath);

// And set its HTML content
panel.webview.html = this.getWebviewContent(this.totalCount, sponsorSvgSrc);
}
}


3 changes: 2 additions & 1 deletion src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import TelemetryReporter from 'vscode-extension-telemetry';
export enum keys {
start = 'start',
restart = 'restart',
stop = 'stop'
stop = 'stop',
sponsorware = 'sponsorware'
}


Expand Down
5 changes: 5 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Returns a random integer between min (included) and max (included)
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
export function getRandomIntInclusive(min:number, max:number):number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}