diff --git a/package-lock.json b/package-lock.json index 1e883f3754..1b9344eb9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "habitica", - "version": "5.40.0", + "version": "5.41.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "habitica", - "version": "5.40.0", + "version": "5.41.1", "hasInstallScript": true, "dependencies": { "@babel/core": "^7.22.10", diff --git a/package.json b/package.json index f727f0ed5d..57c0f4d2aa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "5.40.0", + "version": "5.41.1", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.22.10", diff --git a/test/api/unit/libs/email.test.js b/test/api/unit/libs/email.test.js index 1b34f8d03f..49b2e89538 100644 --- a/test/api/unit/libs/email.test.js +++ b/test/api/unit/libs/email.test.js @@ -150,7 +150,7 @@ describe('emails', () => { sendTxn(mailingInfo, emailType); expect(got.post).to.be.called; - expect(got.post).to.be.calledWith('undefined/job', sinon.match({ + expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({ json: { data: { emailType: sinon.match.same(emailType), @@ -234,7 +234,7 @@ describe('emails', () => { sendTxn(mailingInfo, emailType); expect(got.post).to.be.called; - expect(got.post).to.be.calledWith('undefined/job', sinon.match({ + expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({ json: { data: { emailType: sinon.match.same(emailType), @@ -254,7 +254,7 @@ describe('emails', () => { sendTxn(mailingInfo, emailType, variables); expect(got.post).to.be.called; - expect(got.post).to.be.calledWith('undefined/job', sinon.match({ + expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({ json: { data: { variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'), diff --git a/test/api/unit/libs/payments/apple.test.js b/test/api/unit/libs/payments/apple.test.js index f7435609d1..3f297fe097 100644 --- a/test/api/unit/libs/payments/apple.test.js +++ b/test/api/unit/libs/payments/apple.test.js @@ -12,11 +12,33 @@ const { i18n } = common; describe('Apple Payments', () => { const subKey = 'basic_3mo'; + let iapSetupStub; + let iapValidateStub; + let iapIsValidatedStub; + let iapIsCanceledStub; + let iapIsExpiredStub; + let paymentBuySkuStub; + let iapGetPurchaseDataStub; + let validateGiftMessageStub; + let paymentsCreateSubscritionStub; + + beforeEach(() => { + iapSetupStub = sinon.stub(iap, 'setup').resolves(); + iapValidateStub = sinon.stub(iap, 'validate').resolves({}); + }); + + afterEach(() => { + iap.setup.restore(); + iap.validate.restore(); + iap.isValidated.restore(); + iap.isExpired.restore(); + iap.isCanceled.restore(); + iap.getPurchaseData.restore(); + }); + describe('verifyPurchase', () => { let sku; let user; let token; let receipt; let headers; - let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let - iapGetPurchaseDataStub; let validateGiftMessageStub; beforeEach(() => { token = 'testToken'; @@ -25,13 +47,9 @@ describe('Apple Payments', () => { receipt = `{"token": "${token}", "productId": "${sku}"}`; headers = {}; - iapSetupStub = sinon.stub(iap, 'setup') - .resolves(); - iapValidateStub = sinon.stub(iap, 'validate') - .resolves({}); iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true); - sinon.stub(iap, 'isExpired').returns(false); - sinon.stub(iap, 'isCanceled').returns(false); + iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false); + iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false); iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') .returns([{ productId: 'com.habitrpg.ios.Habitica.21gems', @@ -42,12 +60,6 @@ describe('Apple Payments', () => { }); afterEach(() => { - iap.setup.restore(); - iap.validate.restore(); - iap.isValidated.restore(); - iap.isExpired.restore(); - iap.isCanceled.restore(); - iap.getPurchaseData.restore(); payments.buySkuItem.restore(); gems.validateGiftMessage.restore(); }); @@ -209,9 +221,6 @@ describe('Apple Payments', () => { describe('subscribe', () => { let sub; let sku; let user; let token; let receipt; let headers; let nextPaymentProcessing; - let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; - let paymentsCreateSubscritionStub; let - iapGetPurchaseDataStub; beforeEach(() => { sub = common.content.subscriptionBlocks[subKey]; @@ -223,12 +232,10 @@ describe('Apple Payments', () => { nextPaymentProcessing = moment.utc().add({ days: 2 }); user = new User(); - iapSetupStub = sinon.stub(iap, 'setup') - .resolves(); - iapValidateStub = sinon.stub(iap, 'validate') - .resolves({}); iapIsValidatedStub = sinon.stub(iap, 'isValidated') .returns(true); + iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false); + iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false); iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') .returns([{ expirationDate: moment.utc().subtract({ day: 1 }).toDate(), @@ -250,10 +257,6 @@ describe('Apple Payments', () => { }); afterEach(() => { - iap.setup.restore(); - iap.validate.restore(); - iap.isValidated.restore(); - iap.getPurchaseData.restore(); if (payments.createSubscription.restore) payments.createSubscription.restore(); }); @@ -270,6 +273,29 @@ describe('Apple Payments', () => { }); }); + it('should throw an error if no active subscription is found', async () => { + iap.isCanceled.restore(); + iapIsCanceledStub = sinon.stub(iap, 'isCanceled') + .returns(true); + + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ + expirationDate: moment.utc().add({ day: -2 }).toDate(), + purchaseDate: new Date(), + productId: 'subscription1month', + transactionId: token, + originalTransactionId: token, + }]); + + await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED, + }); + }); + const subOptions = [ { sku: 'subscription1month', @@ -574,8 +600,7 @@ describe('Apple Payments', () => { describe('cancelSubscribe ', () => { let user; let token; let receipt; let headers; let customerId; let expirationDate; - let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let - paymentCancelSubscriptionSpy; + let paymentCancelSubscriptionSpy; beforeEach(async () => { token = 'test-token'; @@ -584,8 +609,7 @@ describe('Apple Payments', () => { customerId = 'test-customerId'; expirationDate = moment.utc(); - iapSetupStub = sinon.stub(iap, 'setup') - .resolves(); + iapValidateStub.restore(); iapValidateStub = sinon.stub(iap, 'validate') .resolves({ expirationDate, @@ -593,8 +617,8 @@ describe('Apple Payments', () => { iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') .returns([{ expirationDate: expirationDate.toDate() }]); iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true); - sinon.stub(iap, 'isCanceled').returns(false); - sinon.stub(iap, 'isExpired').returns(true); + iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false); + iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true); user = new User(); user.profile.name = 'sender'; user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE; @@ -606,13 +630,7 @@ describe('Apple Payments', () => { }); afterEach(() => { - iap.setup.restore(); - iap.validate.restore(); - iap.isValidated.restore(); - iap.isExpired.restore(); - iap.isCanceled.restore(); - iap.getPurchaseData.restore(); - payments.cancelSubscription.restore(); + paymentCancelSubscriptionSpy.restore(); }); it('should throw an error if we are missing a subscription', async () => { @@ -695,6 +713,8 @@ describe('Apple Payments', () => { expect(iapIsValidatedStub).to.be.calledWith({ expirationDate, }); + expect(iapIsCanceledStub).to.be.calledOnce; + expect(iapIsExpiredStub).to.be.calledOnce; expect(iapGetPurchaseDataStub).to.be.calledOnce; expect(paymentCancelSubscriptionSpy).to.be.calledOnce; diff --git a/test/api/unit/libs/payments/google.test.js b/test/api/unit/libs/payments/google.test.js index eb9b3574ba..96fc42b210 100644 --- a/test/api/unit/libs/payments/google.test.js +++ b/test/api/unit/libs/payments/google.test.js @@ -11,12 +11,36 @@ const { i18n } = common; describe('Google Payments', () => { const subKey = 'basic_3mo'; + let iapSetupStub; + let iapValidateStub; + let iapIsValidatedStub; + let paymentBuySkuStub; + let validateGiftMessageStub; + + beforeEach(() => { + iapSetupStub = sinon.stub(iap, 'setup') + .resolves(); + iapIsValidatedStub = sinon.stub(iap, 'isValidated') + .returns(true); + sinon.stub(iap, 'isCanceled').returns(false); + sinon.stub(iap, 'isExpired').returns(false); + paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({}); + validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage'); + }); + + afterEach(() => { + iap.setup.restore(); + iap.validate.restore(); + iap.isValidated.restore(); + iap.isCanceled.restore(); + iap.isExpired.restore(); + payments.buySkuItem.restore(); + gems.validateGiftMessage.restore(); + }); describe('verifyPurchase', () => { let sku; let user; let token; let receipt; let signature; let headers; - let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let - paymentBuySkuStub; let validateGiftMessageStub; beforeEach(() => { sku = 'com.habitrpg.android.habitica.iap.21gems'; @@ -25,21 +49,7 @@ describe('Google Payments', () => { signature = ''; headers = {}; - iapSetupStub = sinon.stub(iap, 'setup') - .resolves(); iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku }); - iapIsValidatedStub = sinon.stub(iap, 'isValidated') - .returns(true); - paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({}); - validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage'); - }); - - afterEach(() => { - iap.setup.restore(); - iap.validate.restore(); - iap.isValidated.restore(); - payments.buySkuItem.restore(); - gems.validateGiftMessage.restore(); }); it('should throw an error if receipt is invalid', async () => { @@ -160,8 +170,7 @@ describe('Google Payments', () => { describe('subscribe', () => { let sub; let sku; let user; let token; let receipt; let signature; let headers; let nextPaymentProcessing; - let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let - paymentsCreateSubscritionStub; + let paymentsCreateSubscritionStub; beforeEach(() => { sub = common.content.subscriptionBlocks[subKey]; @@ -173,19 +182,12 @@ describe('Google Payments', () => { signature = ''; nextPaymentProcessing = moment.utc().add({ days: 2 }); - iapSetupStub = sinon.stub(iap, 'setup') - .resolves(); iapValidateStub = sinon.stub(iap, 'validate') .resolves({}); - iapIsValidatedStub = sinon.stub(iap, 'isValidated') - .returns(true); paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({}); }); afterEach(() => { - iap.setup.restore(); - iap.validate.restore(); - iap.isValidated.restore(); payments.createSubscription.restore(); }); @@ -243,7 +245,7 @@ describe('Google Payments', () => { describe('cancelSubscribe ', () => { let user; let token; let receipt; let signature; let headers; let customerId; let expirationDate; - let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let + let iapGetPurchaseDataStub; let paymentCancelSubscriptionSpy; beforeEach(async () => { @@ -253,17 +255,12 @@ describe('Google Payments', () => { signature = ''; customerId = 'test-customerId'; expirationDate = moment.utc(); - - iapSetupStub = sinon.stub(iap, 'setup') - .resolves(); iapValidateStub = sinon.stub(iap, 'validate') .resolves({ expirationDate, }); iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') .returns([{ expirationDate: expirationDate.toDate(), autoRenewing: false }]); - iapIsValidatedStub = sinon.stub(iap, 'isValidated') - .returns(true); user = new User(); user.profile.name = 'sender'; @@ -276,9 +273,6 @@ describe('Google Payments', () => { }); afterEach(() => { - iap.setup.restore(); - iap.validate.restore(); - iap.isValidated.restore(); iap.getPurchaseData.restore(); payments.cancelSubscription.restore(); }); @@ -308,6 +302,8 @@ describe('Google Payments', () => { }); it('should cancel a user subscription', async () => { + iap.isCanceled.restore(); + iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true); await googlePayments.cancelSubscribe(user, headers); expect(iapSetupStub).to.be.calledOnce; @@ -332,11 +328,20 @@ describe('Google Payments', () => { }); it('should cancel a user subscription with multiple inactive subscriptions', async () => { + iap.isCanceled.restore(); + iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true); const laterDate = moment.utc().add(7, 'days'); iap.getPurchaseData.restore(); iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') - .returns([{ expirationDate, autoRenewing: false }, - { expirationDate: laterDate, autoRenewing: false }, + .returns([{ + startTimeMillis: expirationDate.valueOf(), + expirationDate, + autoRenewing: false, + }, { + startTimeMillis: laterDate.valueOf(), + expirationDate: laterDate, + autoRenewing: false, + }, ]); await googlePayments.cancelSubscribe(user, headers); @@ -365,7 +370,12 @@ describe('Google Payments', () => { iap.getPurchaseData.restore(); iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') .returns([{ autoRenewing: true }]); - await googlePayments.cancelSubscribe(user, headers); + await expect(googlePayments.cancelSubscribe(user, headers)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: googlePayments.constants.RESPONSE_STILL_VALID, + }); expect(iapSetupStub).to.be.calledOnce; expect(iapValidateStub).to.be.calledOnce; @@ -388,8 +398,12 @@ describe('Google Payments', () => { .returns([{ expirationDate, autoRenewing: false }, { autoRenewing: true }, { expirationDate, autoRenewing: false }]); - await googlePayments.cancelSubscribe(user, headers); - + await expect(googlePayments.cancelSubscribe(user, headers)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: googlePayments.constants.RESPONSE_STILL_VALID, + }); expect(iapSetupStub).to.be.calledOnce; expect(iapValidateStub).to.be.calledOnce; expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, { diff --git a/website/client/package-lock.json b/website/client/package-lock.json index 72fda2c65f..00ef1faed5 100644 --- a/website/client/package-lock.json +++ b/website/client/package-lock.json @@ -22,6 +22,7 @@ "eslint-config-habitrpg": "6.2.0", "eslint-plugin-mocha": "5.3.0", "eslint-plugin-vue": "7.20.0", + "ga-gtag": "^1.2.0", "habitica-markdown": "^3.0.0", "hellojs": "^1.20.0", "intro.js": "^7.2.0", @@ -36,12 +37,11 @@ "timers-browserify": "^2.0.12", "uuid": "^9.0.1", "validator": "^13.9.0", - "vite": "^6.0.0", + "vite": "^6.3.6", "vite-plugin-compression2": "^1.3.3", "vue": "^2.7.10", "vue-fragment": "^1.6.0", "vue-mugen-scroll": "^0.2.6", - "vue-plugin-load-script": "^1.3.6", "vue-router": "^3.6.5", "vuedraggable": "^2.24.3", "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0" @@ -5043,6 +5043,20 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5081,6 +5095,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ga-gtag": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ga-gtag/-/ga-gtag-1.2.0.tgz", + "integrity": "sha512-j9gxutMdpGMdwaX1SzOG31Ddm+IGFjeNf+N3Z5g+BBpS8FSXOALlrM+ORIGc/QKszGJEDlw+6PfIsJZICsqsGQ==" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7041,6 +7060,21 @@ "node": ">=18" } }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/popper.js": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", @@ -8417,9 +8451,10 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -8712,11 +8747,6 @@ "node": ">=4" } }, - "node_modules/vue-plugin-load-script": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/vue-plugin-load-script/-/vue-plugin-load-script-1.3.6.tgz", - "integrity": "sha512-O+mpw32dXY3tMBBNKm7/qIByJV1QbHJ+0CXI4GeXx4NHU/Rg+bv7bvzugkEWnYcB/43WvR8ZD2K9KQIeVng1bA==" - }, "node_modules/vue-router": { "version": "3.6.5", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz", diff --git a/website/client/package.json b/website/client/package.json index 60919b8a17..5e59af8942 100644 --- a/website/client/package.json +++ b/website/client/package.json @@ -26,6 +26,7 @@ "eslint-config-habitrpg": "6.2.0", "eslint-plugin-mocha": "5.3.0", "eslint-plugin-vue": "7.20.0", + "ga-gtag": "^1.2.0", "habitica-markdown": "^3.0.0", "hellojs": "^1.20.0", "intro.js": "^7.2.0", @@ -40,12 +41,11 @@ "timers-browserify": "^2.0.12", "uuid": "^9.0.1", "validator": "^13.9.0", - "vite": "^6.0.0", + "vite": "^6.3.6", "vite-plugin-compression2": "^1.3.3", "vue": "^2.7.10", "vue-fragment": "^1.6.0", "vue-mugen-scroll": "^0.2.6", - "vue-plugin-load-script": "^1.3.6", "vue-router": "^3.6.5", "vuedraggable": "^2.24.3", "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0" diff --git a/website/client/src/assets/scss/typography.scss b/website/client/src/assets/scss/typography.scss index f82fed2d72..d20415160f 100644 --- a/website/client/src/assets/scss/typography.scss +++ b/website/client/src/assets/scss/typography.scss @@ -123,6 +123,10 @@ h4 { background-color: $purple-300 !important; } +.bg-yellow-50 { + background-color: $yellow-50 !important; +} + .bg-white { background-color: $white !important; } diff --git a/website/client/src/components/admin/admin-panel/search.vue b/website/client/src/components/admin/admin-panel/search.vue index 066d4727fb..812cb99cbb 100644 --- a/website/client/src/components/admin/admin-panel/search.vue +++ b/website/client/src/components/admin/admin-panel/search.vue @@ -81,7 +81,7 @@ export default { watch: { userIdentifier () { this.isSearching = true; - this.$store.dispatch('adminPanel:searchUsers', { userIdentifier: this.userIdentifier }).then(users => { + this.$store.dispatch('admin:searchUsers', { userIdentifier: this.userIdentifier }).then(users => { this.isSearching = false; if (users.length === 1) { this.loadUser(users[0]._id); diff --git a/website/client/src/components/admin/admin-panel/user-support/index.vue b/website/client/src/components/admin/admin-panel/user-support/index.vue index ce9e090de2..8b836a309e 100644 --- a/website/client/src/components/admin/admin-panel/user-support/index.vue +++ b/website/client/src/components/admin/admin-panel/user-support/index.vue @@ -5,6 +5,12 @@ class="row" >
+ Are you sure you want to delete this member? +
++ Please note: This action cannot be undone! +
+